mixdog 0.9.21 → 0.9.23
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 +1 -4
- package/package.json +1 -1
- package/scripts/channel-daemon-smoke.mjs +165 -0
- package/scripts/channel-daemon-stub.mjs +69 -0
- package/scripts/statusline-quota-hysteresis-test.mjs +37 -0
- package/scripts/tool-smoke.mjs +30 -17
- package/src/runtime/agent/orchestrator/mcp/child-tree.mjs +187 -0
- package/src/runtime/agent/orchestrator/mcp/client.mjs +40 -3
- package/src/runtime/agent/orchestrator/providers/openai-compat-stream.mjs +1 -1
- package/src/runtime/agent/orchestrator/providers/openai-compat-wire.mjs +5 -3
- package/src/runtime/agent/orchestrator/providers/openai-oauth-http-sse.mjs +2 -2
- package/src/runtime/agent/orchestrator/providers/openai-oauth.mjs +2 -2
- package/src/runtime/agent/orchestrator/providers/openai-ws-stream.mjs +1 -1
- package/src/runtime/agent/orchestrator/session/loop/completion-guards.mjs +3 -2
- package/src/runtime/agent/orchestrator/session/loop/deferred-call-through.mjs +6 -3
- package/src/runtime/agent/orchestrator/session/loop/tool-helpers.mjs +1 -1
- package/src/runtime/agent/orchestrator/session/manager/compaction-runner.mjs +37 -4
- package/src/runtime/agent/orchestrator/session/manager/session-lifecycle.mjs +7 -0
- package/src/runtime/agent/orchestrator/session/store.mjs +30 -14
- package/src/runtime/agent/orchestrator/tools/bash-session.mjs +10 -10
- package/src/runtime/agent/orchestrator/tools/builtin/bash-tool.mjs +8 -6
- package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +14 -1
- package/src/runtime/agent/orchestrator/tools/builtin/shell-job-paths.mjs +1 -1
- package/src/runtime/agent/orchestrator/tools/builtin/shell-jobs.mjs +24 -11
- package/src/runtime/agent/orchestrator/tools/builtin/shell-runtime.mjs +6 -5
- package/src/runtime/agent/orchestrator/tools/patch/orchestrator.mjs +23 -13
- package/src/runtime/agent/orchestrator/tools/shell-command.mjs +56 -1
- package/src/runtime/channels/lib/owned-runtime.mjs +126 -167
- package/src/runtime/channels/lib/owner-heartbeat.mjs +11 -60
- package/src/runtime/channels/lib/parent-bridge.mjs +16 -1
- package/src/runtime/channels/lib/runtime-paths.mjs +32 -113
- package/src/runtime/channels/lib/seat-lock.mjs +196 -0
- package/src/runtime/channels/lib/session-discovery.mjs +2 -1
- package/src/runtime/channels/lib/tool-dispatch.mjs +24 -8
- package/src/runtime/channels/lib/worker-main.mjs +42 -11
- package/src/runtime/memory/index.mjs +54 -7
- package/src/runtime/memory/lib/embedding-warmup.mjs +7 -0
- package/src/runtime/memory/lib/pg/process.mjs +85 -40
- package/src/runtime/memory/lib/pg/supervisor.mjs +82 -17
- package/src/runtime/shared/atomic-file.mjs +44 -10
- package/src/runtime/shared/tool-primitives.mjs +31 -1
- package/src/runtime/shared/tool-surface.mjs +23 -13
- package/src/session-runtime/config-lifecycle.mjs +48 -7
- package/src/session-runtime/lifecycle-api.mjs +9 -0
- package/src/session-runtime/mcp-glue.mjs +63 -1
- package/src/session-runtime/resource-api.mjs +62 -8
- package/src/session-runtime/runtime-core.mjs +32 -2
- package/src/session-runtime/session-text.mjs +41 -0
- package/src/session-runtime/session-turn-api.mjs +24 -0
- package/src/session-runtime/settings-api.mjs +8 -1
- package/src/session-runtime/tool-catalog.mjs +306 -38
- package/src/session-runtime/tool-defs.mjs +7 -7
- package/src/session-runtime/workflow.mjs +2 -1
- package/src/standalone/channel-daemon-client.mjs +224 -0
- package/src/standalone/channel-daemon-transport.mjs +351 -0
- package/src/standalone/channel-daemon.mjs +139 -0
- package/src/standalone/channel-worker.mjs +213 -4
- package/src/standalone/hook-bus.mjs +71 -3
- package/src/tui/App.jsx +105 -17
- package/src/tui/app/clipboard.mjs +39 -19
- package/src/tui/app/doctor.mjs +57 -0
- package/src/tui/app/extension-pickers.mjs +53 -9
- package/src/tui/app/maintenance-pickers.mjs +0 -5
- package/src/tui/app/slash-dispatch.mjs +4 -4
- package/src/tui/app/text-layout.mjs +11 -0
- package/src/tui/app/use-mouse-input.mjs +235 -51
- package/src/tui/app/use-prompt-handlers.mjs +49 -30
- package/src/tui/app/use-transcript-scroll.mjs +124 -27
- package/src/tui/app/use-transcript-window.mjs +55 -1
- package/src/tui/components/Message.jsx +1 -1
- package/src/tui/components/PromptInput.jsx +3 -1
- package/src/tui/components/QueuedCommands.jsx +21 -10
- package/src/tui/components/ToolExecution.jsx +2 -2
- package/src/tui/dist/index.mjs +653 -310
- package/src/tui/engine/session-api.mjs +17 -7
- package/src/tui/engine/session-flow.mjs +6 -0
- package/src/tui/engine.mjs +8 -0
- package/src/tui/index.jsx +62 -18
- package/src/tui/paste-attachments.mjs +26 -0
- package/src/ui/statusline.mjs +45 -5
- package/src/ui/tool-card.mjs +8 -1
- package/src/vendor/statusline/bin/statusline-route.mjs +23 -7
- package/src/workflows/bench/WORKFLOW.md +46 -0
- package/src/workflows/default/WORKFLOW.md +6 -0
- package/src/workflows/solo/WORKFLOW.md +5 -0
- package/vendor/ink/build/ink.js +23 -1
- package/vendor/ink/build/output.js +154 -71
- package/vendor/ink/build/render-node-to-output.js +44 -2
- package/vendor/ink/build/render.js +4 -0
- package/vendor/ink/build/renderer.js +4 -1
|
@@ -4,6 +4,7 @@ import { join } from 'path';
|
|
|
4
4
|
import { tmpdir } from 'os';
|
|
5
5
|
import { randomUUID } from 'crypto';
|
|
6
6
|
import { smartReadTruncate } from '../tools/builtin/read-formatting.mjs';
|
|
7
|
+
import { shutdownStdioChild } from './child-tree.mjs';
|
|
7
8
|
// --- Types ---
|
|
8
9
|
/** Known auto-detect targets: port file path relative to tmpdir.
|
|
9
10
|
* Note: `mixdog` used to self-loopback via active-instance.json's
|
|
@@ -182,7 +183,7 @@ export async function executeMcpTool(name, args) {
|
|
|
182
183
|
mcpLog(`[mcp-client] Tool call failed, attempting reconnect...\n`);
|
|
183
184
|
await new Promise(r => setTimeout(r, 500));
|
|
184
185
|
try {
|
|
185
|
-
await server
|
|
186
|
+
await _closeServer(server);
|
|
186
187
|
} catch { /* ignore close error */ }
|
|
187
188
|
try {
|
|
188
189
|
await connectServer(serverName, server.cfg);
|
|
@@ -241,7 +242,9 @@ async function _callToolWithTimeout(server, toolName, args) {
|
|
|
241
242
|
}
|
|
242
243
|
const timeout = new Promise((_, rej) => {
|
|
243
244
|
timer = setTimeout(() => {
|
|
244
|
-
|
|
245
|
+
// Route through the full tree-shutdown path so a timed-out stdio
|
|
246
|
+
// server never orphans grandchildren. Fire-and-forget.
|
|
247
|
+
try { _closeServer(server).catch(() => {}); } catch { /* ignore */ }
|
|
245
248
|
const err = new Error(`MCP tool call timed out after ${timeoutMs}ms (server="${server.name}", tool="${toolName}")`);
|
|
246
249
|
err.code = 'EMCPTOOLTIMEOUT';
|
|
247
250
|
err.serverName = server.name;
|
|
@@ -338,13 +341,47 @@ export function mcpToolHasField(name, field) {
|
|
|
338
341
|
export async function disconnectAll() {
|
|
339
342
|
for (const [name, server] of servers) {
|
|
340
343
|
try {
|
|
341
|
-
await server
|
|
344
|
+
await _closeServer(server);
|
|
342
345
|
}
|
|
343
346
|
catch { /* ignore */ }
|
|
344
347
|
servers.delete(name);
|
|
345
348
|
}
|
|
346
349
|
_invalidateMcpToolFieldMemo();
|
|
347
350
|
}
|
|
351
|
+
/**
|
|
352
|
+
* Disconnect a single MCP server by name. No-op (returns false) when the
|
|
353
|
+
* server is not in the live registry; otherwise closes its transport, removes
|
|
354
|
+
* it, and invalidates the tool-field memo. Lets callers toggle one server
|
|
355
|
+
* without a full disconnectAll()/reconnect cycle.
|
|
356
|
+
*/
|
|
357
|
+
export async function disconnectMcpServer(name) {
|
|
358
|
+
const server = servers.get(name);
|
|
359
|
+
if (!server) return false;
|
|
360
|
+
try {
|
|
361
|
+
await _closeServer(server);
|
|
362
|
+
}
|
|
363
|
+
catch { /* ignore */ }
|
|
364
|
+
servers.delete(name);
|
|
365
|
+
_invalidateMcpToolFieldMemo();
|
|
366
|
+
return true;
|
|
367
|
+
}
|
|
368
|
+
/**
|
|
369
|
+
* Close a single server: for stdio transports first shut down the full child
|
|
370
|
+
* process tree (close stdin -> grace -> tree kill) so wrapper chains such as
|
|
371
|
+
* uvx/npx/uv never orphan grandchildren, then release the SDK client. The
|
|
372
|
+
* tree teardown runs before client.close() because the SDK's own close()
|
|
373
|
+
* only kills the direct child and discards the pid we need to walk the tree.
|
|
374
|
+
*/
|
|
375
|
+
async function _closeServer(server) {
|
|
376
|
+
const transport = server?.transport;
|
|
377
|
+
// Live stdio transports expose the spawned ChildProcess on _process.
|
|
378
|
+
if (transport && transport._process) {
|
|
379
|
+
try { await shutdownStdioChild(transport); }
|
|
380
|
+
catch { /* ignore */ }
|
|
381
|
+
}
|
|
382
|
+
try { await server.client.close(); }
|
|
383
|
+
catch { /* ignore */ }
|
|
384
|
+
}
|
|
348
385
|
async function connectServer(name, cfg) {
|
|
349
386
|
const {
|
|
350
387
|
Client,
|
|
@@ -514,7 +514,7 @@ function handleCompatResponsesStreamEvent(event, state, { label, parseResponsesT
|
|
|
514
514
|
: parseCompletedToolCallArgumentsJson(item.arguments || '{}', label, { id: callId, name: 'tool_search', finishReason: 'done' });
|
|
515
515
|
const call = {
|
|
516
516
|
id: callId,
|
|
517
|
-
name: '
|
|
517
|
+
name: 'load_tool',
|
|
518
518
|
// Schema is a plain object ({query,select,limit}); an array must
|
|
519
519
|
// never pass through as args.
|
|
520
520
|
arguments: (_tsArgs && typeof _tsArgs === 'object' && !Array.isArray(_tsArgs)) ? _tsArgs : {},
|
|
@@ -107,7 +107,9 @@ export function toOpenAITools(tools) {
|
|
|
107
107
|
|
|
108
108
|
export function toResponsesTools(tools) {
|
|
109
109
|
return tools.map((t) => {
|
|
110
|
-
|
|
110
|
+
// load_tool advertises as the OpenAI-native `tool_search` wire type
|
|
111
|
+
// (legacy 'tool_search' name still accepted for back-compat).
|
|
112
|
+
if (t?.name === 'load_tool' || t?.name === 'tool_search') {
|
|
111
113
|
return {
|
|
112
114
|
type: 'tool_search',
|
|
113
115
|
execution: 'client',
|
|
@@ -193,7 +195,7 @@ export function parseResponsesToolCalls(response, label) {
|
|
|
193
195
|
: parseCompletedToolCallArgumentsJson(item.arguments || '{}', label, { id: item.call_id || item.id, name: 'tool_search', finishReason });
|
|
194
196
|
out.push({
|
|
195
197
|
id: item.call_id || item.id,
|
|
196
|
-
name: '
|
|
198
|
+
name: 'load_tool',
|
|
197
199
|
// Schema is a plain object ({query,select,limit}); an array
|
|
198
200
|
// (parsed JSON or passthrough) must never pass through as args.
|
|
199
201
|
arguments: (_tsArgs && typeof _tsArgs === 'object' && !Array.isArray(_tsArgs)) ? _tsArgs : {},
|
|
@@ -294,7 +296,7 @@ export function toResponsesInputMessage(m, pendingToolMedia = null, customToolCa
|
|
|
294
296
|
const items = [];
|
|
295
297
|
if (m.content) items.push({ role: 'assistant', content: normalizeContentForOpenAIResponses(m.content, { role: 'assistant' }) });
|
|
296
298
|
for (const tc of m.toolCalls) {
|
|
297
|
-
if (tc.nativeType === 'tool_search_call' || tc.name === 'tool_search') {
|
|
299
|
+
if (tc.nativeType === 'tool_search_call' || tc.name === 'load_tool' || tc.name === 'tool_search') {
|
|
298
300
|
items.push({
|
|
299
301
|
type: 'tool_search_call',
|
|
300
302
|
call_id: tc.id,
|
|
@@ -451,7 +451,7 @@ export async function sendViaHttpSse({
|
|
|
451
451
|
}
|
|
452
452
|
const call = {
|
|
453
453
|
id: callId,
|
|
454
|
-
name: '
|
|
454
|
+
name: 'load_tool',
|
|
455
455
|
arguments: args,
|
|
456
456
|
nativeType: 'tool_search_call',
|
|
457
457
|
};
|
|
@@ -501,7 +501,7 @@ export async function sendViaHttpSse({
|
|
|
501
501
|
// mistakes it for a function call by id collision/empty id.
|
|
502
502
|
if (event.item.id) {
|
|
503
503
|
pendingCalls.set(event.item.id, {
|
|
504
|
-
name: '
|
|
504
|
+
name: 'load_tool',
|
|
505
505
|
callId: event.item.call_id || '',
|
|
506
506
|
kind: 'tool_search',
|
|
507
507
|
});
|
|
@@ -444,7 +444,7 @@ function convertMessagesToResponsesInput(messages, opts = {}) {
|
|
|
444
444
|
// reasoning in `input` triggers "Duplicate item".
|
|
445
445
|
if (m.content) out.push({ role: 'assistant', content: normalizeContentForOpenAIResponses(m.content, { role: 'assistant' }) });
|
|
446
446
|
for (const tc of m.toolCalls) {
|
|
447
|
-
if (tc.nativeType === 'tool_search_call' || tc.name === 'tool_search') {
|
|
447
|
+
if (tc.nativeType === 'tool_search_call' || tc.name === 'load_tool' || tc.name === 'tool_search') {
|
|
448
448
|
out.push({
|
|
449
449
|
type: 'tool_search_call',
|
|
450
450
|
call_id: tc.id,
|
|
@@ -480,7 +480,7 @@ function convertMessagesToResponsesInput(messages, opts = {}) {
|
|
|
480
480
|
}
|
|
481
481
|
|
|
482
482
|
function toOpenAIResponsesTool(t) {
|
|
483
|
-
if (t?.name === 'tool_search') {
|
|
483
|
+
if (t?.name === 'load_tool' || t?.name === 'tool_search') {
|
|
484
484
|
return {
|
|
485
485
|
type: 'tool_search',
|
|
486
486
|
execution: 'client',
|
|
@@ -355,7 +355,7 @@ export async function _streamResponse({
|
|
|
355
355
|
if (!callId || toolCalls.some((call) => call.id === callId)) return;
|
|
356
356
|
const call = {
|
|
357
357
|
id: callId,
|
|
358
|
-
name: '
|
|
358
|
+
name: 'load_tool',
|
|
359
359
|
arguments: parseToolSearchArgs(item.arguments),
|
|
360
360
|
nativeType: 'tool_search_call',
|
|
361
361
|
};
|
|
@@ -24,8 +24,9 @@ export function crossTurnSignature(name, args) {
|
|
|
24
24
|
|
|
25
25
|
// Tool names that are non-eager (no readOnlyHint) but are NOT edits/progress —
|
|
26
26
|
// they must not reset the escalation ladder's "zero edit" condition. Skill /
|
|
27
|
-
// recall / agent / task / cwd /
|
|
28
|
-
|
|
27
|
+
// recall / agent / task / cwd / load_tool are exploration/meta plumbing.
|
|
28
|
+
// (tool_search kept as legacy alias for old transcripts.)
|
|
29
|
+
const NON_PROGRESS_TOOLS = new Set(['Skill', 'recall', 'agent', 'task', 'cwd', 'load_tool', 'tool_search']);
|
|
29
30
|
|
|
30
31
|
// True when a successfully-executed tool represents real edit/progress. A tool
|
|
31
32
|
// counts as progress only if its def lacks readOnlyHint (not eager) AND it is
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// Deferred catalog call-through: inactive catalog tool_use → discovery bookkeeping
|
|
2
2
|
// then normal executeTool routing (runtime errors only; no pre-dispatch schema).
|
|
3
3
|
import { clean } from '../../../../../session-runtime/session-text.mjs';
|
|
4
|
-
import { isReadonlySelectable, selectDeferredTools } from '../../../../../session-runtime/tool-catalog.mjs';
|
|
4
|
+
import { deferredCatalogUnion, isReadonlySelectable, selectDeferredTools } from '../../../../../session-runtime/tool-catalog.mjs';
|
|
5
5
|
|
|
6
6
|
/** Skill-list plumbing only; mutation/MCP/builtins must use the catalog+mode gate. */
|
|
7
7
|
const INACTIVE_INFRA_BYPASS = new Set(['skills_list', 'skill_view']);
|
|
@@ -13,7 +13,10 @@ function isActiveSessionTool(session, name) {
|
|
|
13
13
|
}
|
|
14
14
|
|
|
15
15
|
function lookupDeferredCatalogTool(session, name) {
|
|
16
|
-
|
|
16
|
+
// Union of the boot-frozen catalog and the late-connected MCP catalog so a
|
|
17
|
+
// direct call to a tool whose server connected after boot resolves and
|
|
18
|
+
// auto-loads (selectDeferredTools promotes it onto session.tools).
|
|
19
|
+
const catalog = deferredCatalogUnion(session);
|
|
17
20
|
const key = clean(name);
|
|
18
21
|
if (!key) return null;
|
|
19
22
|
for (const tool of catalog) {
|
|
@@ -66,7 +69,7 @@ export function prepareDeferredToolCallThrough(sessionRef, name, _args) {
|
|
|
66
69
|
if (resolvedMode === null) {
|
|
67
70
|
if (!isReadonlySelectable(tool)) {
|
|
68
71
|
return denyDeferredCallThrough(
|
|
69
|
-
`Error: tool "${toolLabel}" is deferred and cannot be auto-loaded without a resolved tool mode;
|
|
72
|
+
`Error: tool "${toolLabel}" is deferred and cannot be auto-loaded without a resolved tool mode; load it with load_tool names:["${toolLabel}"].`,
|
|
70
73
|
);
|
|
71
74
|
}
|
|
72
75
|
} else if (resolvedMode === 'readonly' && !isReadonlySelectable(tool)) {
|
|
@@ -81,7 +81,7 @@ export function resolveToolResultAfterHook(originalResult, hookResult) {
|
|
|
81
81
|
}
|
|
82
82
|
|
|
83
83
|
export function parseNativeToolSearchPayload(toolName, result) {
|
|
84
|
-
if (toolName !== 'tool_search' || typeof result !== 'string') return null;
|
|
84
|
+
if ((toolName !== 'load_tool' && toolName !== 'tool_search') || typeof result !== 'string') return null;
|
|
85
85
|
try {
|
|
86
86
|
const parsed = JSON.parse(result);
|
|
87
87
|
const native = parsed?.nativeToolSearch;
|
|
@@ -74,6 +74,39 @@ function recallMemoryTimeoutMs(session) {
|
|
|
74
74
|
// misconfigured tiny value can't turn the bound into a busy no-wait.
|
|
75
75
|
return Math.max(250, configured || RECALL_MEMORY_CALL_TIMEOUT_MS);
|
|
76
76
|
}
|
|
77
|
+
// Cold-start allowance (clear/manual path only): a booting memory daemon can
|
|
78
|
+
// miss the tight first bound (waitForPort + first-RPC warmup ~2-10s). On a
|
|
79
|
+
// timeout we retry ONCE with a longer bound before honoring the bail-to-
|
|
80
|
+
// semantic contract, so a rebooting runtime succeeds instead of instantly
|
|
81
|
+
// failing. Non-timeout errors and outer aborts propagate immediately.
|
|
82
|
+
// 15s: memory boot is ~2-4s warm-cache / ~10s worst since the PG fast-start
|
|
83
|
+
// fix; keeping this tight bounds the clear path's worst case (2 memory calls
|
|
84
|
+
// retried + 120s semantic) under the TUI auto-clear watchdog (180s).
|
|
85
|
+
const RECALL_COLD_START_TIMEOUT_MS = 15_000;
|
|
86
|
+
function isTimeoutError(err) {
|
|
87
|
+
return typeof err?.message === 'string' && err.message.includes('timed out after');
|
|
88
|
+
}
|
|
89
|
+
async function callMemoryColdStart(args, callerCtx, timeoutMs) {
|
|
90
|
+
try {
|
|
91
|
+
return await callMemoryBounded(args, callerCtx, timeoutMs);
|
|
92
|
+
} catch (err) {
|
|
93
|
+
if (!isTimeoutError(err) || callerCtx?.signal?.aborted) throw err;
|
|
94
|
+
const coldMs = Math.max(timeoutMs, RECALL_COLD_START_TIMEOUT_MS);
|
|
95
|
+
if (coldMs <= timeoutMs) throw err;
|
|
96
|
+
try { process.stderr.write(`[session] recall-fasttrack ${args?.action || 'call'} cold-start retry (${timeoutMs}ms -> ${coldMs}ms)\n`); } catch {}
|
|
97
|
+
return await callMemoryBounded(args, callerCtx, coldMs);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
// Semantic-compact timeout scales with transcript size (clear/manual path):
|
|
101
|
+
// default max(30s, ~10s per 25k estimated message tokens) capped at 120s, so a
|
|
102
|
+
// large (~100k-token) transcript no longer dies on a fixed 30s bound.
|
|
103
|
+
// session.compaction.timeoutMs still overrides.
|
|
104
|
+
function semanticCompactTimeoutMs(session, messageTokens) {
|
|
105
|
+
const override = positiveContextWindow(session?.compaction?.timeoutMs);
|
|
106
|
+
if (override) return override;
|
|
107
|
+
const scaled = Math.ceil((messageTokens || 0) / 25_000) * 10_000;
|
|
108
|
+
return Math.min(120_000, Math.max(30_000, scaled));
|
|
109
|
+
}
|
|
77
110
|
async function callMemoryBounded(args, callerCtx, timeoutMs) {
|
|
78
111
|
const ac = new AbortController();
|
|
79
112
|
const outer = callerCtx?.signal;
|
|
@@ -118,7 +151,7 @@ async function runRecallFastTrackForSession(session, messages, opts = {}) {
|
|
|
118
151
|
|| Math.max(500, Math.min(5000, messages.length || 0));
|
|
119
152
|
const memoryTimeoutMs = recallMemoryTimeoutMs(session);
|
|
120
153
|
try {
|
|
121
|
-
await
|
|
154
|
+
await callMemoryColdStart({
|
|
122
155
|
action: 'ingest_session',
|
|
123
156
|
sessionId,
|
|
124
157
|
messages,
|
|
@@ -140,7 +173,7 @@ async function runRecallFastTrackForSession(session, messages, opts = {}) {
|
|
|
140
173
|
// digest.
|
|
141
174
|
let recallText = '';
|
|
142
175
|
try {
|
|
143
|
-
const browsed = await
|
|
176
|
+
const browsed = await callMemoryColdStart({
|
|
144
177
|
action: 'search',
|
|
145
178
|
sessionId,
|
|
146
179
|
limit: positiveContextWindow(session?.compaction?.recallDigestLimit) || 30,
|
|
@@ -298,7 +331,7 @@ export async function runSessionCompaction(session, opts = {}) {
|
|
|
298
331
|
signal: opts.signal || null,
|
|
299
332
|
promptCacheKey: session.promptCacheKey || null,
|
|
300
333
|
providerCacheKey: session.promptCacheKey || null,
|
|
301
|
-
timeoutMs:
|
|
334
|
+
timeoutMs: semanticCompactTimeoutMs(session, beforeMessageTokens),
|
|
302
335
|
tailTurns: positiveContextWindow(session.compaction?.tailTurns) || 2,
|
|
303
336
|
keepTokens: positiveContextWindow(session.compaction?.keepTokens ?? session.compaction?.keep?.tokens),
|
|
304
337
|
preserveRecentTokens: positiveContextWindow(session.compaction?.preserveRecentTokens),
|
|
@@ -341,7 +374,7 @@ export async function runSessionCompaction(session, opts = {}) {
|
|
|
341
374
|
signal: opts.signal || null,
|
|
342
375
|
promptCacheKey: session.promptCacheKey || null,
|
|
343
376
|
providerCacheKey: session.promptCacheKey || null,
|
|
344
|
-
timeoutMs:
|
|
377
|
+
timeoutMs: semanticCompactTimeoutMs(session, beforeMessageTokens),
|
|
345
378
|
tailTurns: positiveContextWindow(session.compaction?.tailTurns) || 2,
|
|
346
379
|
keepTokens: positiveContextWindow(session.compaction?.keepTokens ?? session.compaction?.keep?.tokens),
|
|
347
380
|
preserveRecentTokens: positiveContextWindow(session.compaction?.preserveRecentTokens),
|
|
@@ -314,6 +314,11 @@ export function createSession(opts) {
|
|
|
314
314
|
},
|
|
315
315
|
tools,
|
|
316
316
|
preset: toolPreset,
|
|
317
|
+
// Persisted so the deferred call-through gate (deferred-call-through.mjs
|
|
318
|
+
// resolveDeferredSelectMode) can resolve the session's tool mode; without
|
|
319
|
+
// this every session read `undefined` and write-capable deferred tools
|
|
320
|
+
// (e.g. MCP) were permanently denied auto-promotion.
|
|
321
|
+
toolSpec,
|
|
317
322
|
presetName: presetObj?.name || null,
|
|
318
323
|
effort,
|
|
319
324
|
fast,
|
|
@@ -459,6 +464,8 @@ export async function resumeSession(sessionId, preset) {
|
|
|
459
464
|
if (ownerIsAgent) {
|
|
460
465
|
toolsForRouting = applyToolPermissionNarrowing(toolsForRouting, session.toolPermission, session.agent || null);
|
|
461
466
|
}
|
|
467
|
+
// Keep the persisted tool mode in sync on resume (see createSession note).
|
|
468
|
+
session.toolSpec = toolSpec;
|
|
462
469
|
session.tools = finalizeSessionToolList(toolsForRouting, {
|
|
463
470
|
schemaAllowedTools: Array.isArray(session.schemaAllowedTools) ? session.schemaAllowedTools : null,
|
|
464
471
|
disallowedTools: getHiddenAgent(session.agent || null) ? ['Skill'] : null,
|
|
@@ -45,17 +45,20 @@ const _lastSaveError = new Map(); // id -> { message, at }
|
|
|
45
45
|
// starving the model of the image mid-conversation. Returns the same object
|
|
46
46
|
// reference when nothing changed (no-image sessions pay only a shallow scan).
|
|
47
47
|
function _sessionForDisk(session) {
|
|
48
|
-
// Strip
|
|
49
|
-
//
|
|
50
|
-
//
|
|
51
|
-
// serialized
|
|
52
|
-
//
|
|
53
|
-
|
|
54
|
-
|
|
48
|
+
// Strip transient in-flight aliases askSession sets for the turn duration:
|
|
49
|
+
// - liveTurnMessages: live working transcript (so contextStatus() can
|
|
50
|
+
// estimate live context growth) — a duplicate of the working transcript
|
|
51
|
+
// that must never be serialized (mid-turn saves would bloat the file and
|
|
52
|
+
// persist a non-canonical message array).
|
|
53
|
+
// - toolApprovalHook: the askOpts.onToolApproval callback wired for the
|
|
54
|
+
// turn — a function that must never be serialized.
|
|
55
|
+
const hasTransient = session && typeof session === 'object'
|
|
56
|
+
&& (Object.prototype.hasOwnProperty.call(session, 'liveTurnMessages')
|
|
57
|
+
|| Object.prototype.hasOwnProperty.call(session, 'toolApprovalHook'));
|
|
55
58
|
const messages = Array.isArray(session?.messages) ? session.messages : null;
|
|
56
59
|
if (!messages || messages.length === 0) {
|
|
57
|
-
if (!
|
|
58
|
-
const { liveTurnMessages:
|
|
60
|
+
if (!hasTransient) return session;
|
|
61
|
+
const { liveTurnMessages: _dropLTM, toolApprovalHook: _dropTAH, ...rest } = session;
|
|
59
62
|
return rest;
|
|
60
63
|
}
|
|
61
64
|
let changed = false;
|
|
@@ -66,11 +69,11 @@ function _sessionForDisk(session) {
|
|
|
66
69
|
return m;
|
|
67
70
|
});
|
|
68
71
|
if (!changed) {
|
|
69
|
-
if (!
|
|
70
|
-
const { liveTurnMessages:
|
|
72
|
+
if (!hasTransient) return session;
|
|
73
|
+
const { liveTurnMessages: _dropLTM, toolApprovalHook: _dropTAH, ...rest } = session;
|
|
71
74
|
return rest;
|
|
72
75
|
}
|
|
73
|
-
const { liveTurnMessages:
|
|
76
|
+
const { liveTurnMessages: _dropLTM, toolApprovalHook: _dropTAH, ...rest } = session;
|
|
74
77
|
return { ...rest, messages: out };
|
|
75
78
|
}
|
|
76
79
|
|
|
@@ -305,13 +308,26 @@ export function saveSessionAsync(session, opts) {
|
|
|
305
308
|
setLiveSession(session);
|
|
306
309
|
const reqId = ++_saveWorkerReqId;
|
|
307
310
|
const safeOpts = opts || null;
|
|
311
|
+
// The Worker `postMessage` below structured-clones the whole session on the
|
|
312
|
+
// main thread. `session.liveTurnMessages` (live working transcript) and
|
|
313
|
+
// `session.toolApprovalHook` (askOpts.onToolApproval callback) are transient
|
|
314
|
+
// in-flight aliases askSession sets for the turn duration; both carry
|
|
315
|
+
// non-cloneable values (a function, and raw messages that can hold functions),
|
|
316
|
+
// which makes structuredClone throw "could not be cloned" for every mid-turn
|
|
317
|
+
// iteration save. The worker strips both via _sessionForDisk anyway, so drop
|
|
318
|
+
// them from the cloned payload here WITHOUT mutating the live session object.
|
|
319
|
+
const clonePayload = (session && typeof session === 'object'
|
|
320
|
+
&& (Object.prototype.hasOwnProperty.call(session, 'liveTurnMessages')
|
|
321
|
+
|| Object.prototype.hasOwnProperty.call(session, 'toolApprovalHook')))
|
|
322
|
+
? (() => { const { liveTurnMessages: _dropLTM, toolApprovalHook: _dropTAH, ...rest } = session; return rest; })()
|
|
323
|
+
: session;
|
|
308
324
|
return new Promise((resolve, reject) => {
|
|
309
325
|
// Persist {session, opts} so drainSessionStore can sync-flush
|
|
310
326
|
// outstanding writes if process exit interrupts the worker queue.
|
|
311
|
-
_saveWorkerPending.set(reqId, { resolve, reject, session, opts: safeOpts });
|
|
327
|
+
_saveWorkerPending.set(reqId, { resolve, reject, session: clonePayload, opts: safeOpts });
|
|
312
328
|
try {
|
|
313
329
|
const w = _getOrSpawnWorker();
|
|
314
|
-
w.postMessage({ session, opts: safeOpts, reqId });
|
|
330
|
+
w.postMessage({ session: clonePayload, opts: safeOpts, reqId });
|
|
315
331
|
// Ref AFTER successful postMessage so a queue/throw failure path
|
|
316
332
|
// does not leave the worker held alive with no pending message.
|
|
317
333
|
// Paired with the unref in the message handler when count hits 0.
|
|
@@ -40,7 +40,7 @@ import { spawn } from 'node:child_process';
|
|
|
40
40
|
import * as nodeUtil from 'node:util';
|
|
41
41
|
import { existsSync } from 'node:fs';
|
|
42
42
|
import { randomUUID } from 'node:crypto';
|
|
43
|
-
import { invalidateBuiltinResultCache, analyzeShellCommandEffects
|
|
43
|
+
import { invalidateBuiltinResultCache, analyzeShellCommandEffects } from './builtin.mjs';
|
|
44
44
|
import { markCodeGraphDirtyPaths, drainCodeGraphCache } from './code-graph-state.mjs';
|
|
45
45
|
import { maybeRewriteWmicProcessCommand } from './shell-policy.mjs';
|
|
46
46
|
import { _maybeEncodePowerShellCommand } from './shell-command.mjs';
|
|
@@ -51,13 +51,15 @@ import { startChildGuardian } from '../../../shared/child-guardian.mjs';
|
|
|
51
51
|
|
|
52
52
|
globalThis.__mixdogBashSessionRuntimeLoaded = true;
|
|
53
53
|
|
|
54
|
-
//
|
|
55
|
-
// 600 s
|
|
56
|
-
//
|
|
57
|
-
//
|
|
58
|
-
//
|
|
59
|
-
const
|
|
60
|
-
const
|
|
54
|
+
// Claude Code parity (refs/claude-code src/utils/timeouts.ts): default 120 s
|
|
55
|
+
// (2 min), max 600 s (10 min), BASH_DEFAULT_TIMEOUT_MS / BASH_MAX_TIMEOUT_MS
|
|
56
|
+
// env overrides (max floored at default). Matches the one-shot bash tool
|
|
57
|
+
// (builtin/bash-tool.mjs); per-call `timeout` still overrides, capped at
|
|
58
|
+
// MAX_TIMEOUT_MS.
|
|
59
|
+
const _envDefaultTimeout = parseInt(process.env.BASH_DEFAULT_TIMEOUT_MS ?? '', 10);
|
|
60
|
+
const DEFAULT_TIMEOUT_MS = _envDefaultTimeout > 0 ? _envDefaultTimeout : 120_000;
|
|
61
|
+
const _envMaxTimeout = parseInt(process.env.BASH_MAX_TIMEOUT_MS ?? '', 10);
|
|
62
|
+
const MAX_TIMEOUT_MS = Math.max(_envMaxTimeout > 0 ? _envMaxTimeout : 600_000, DEFAULT_TIMEOUT_MS);
|
|
61
63
|
const IDLE_TIMEOUT_MS = 5 * 60_000;
|
|
62
64
|
const MAX_SESSIONS = 10;
|
|
63
65
|
const STDERR_DRAIN_MS = 25;
|
|
@@ -643,8 +645,6 @@ async function bash_session(args, cwd = process.cwd(), opts = {}) {
|
|
|
643
645
|
}
|
|
644
646
|
return requestedCwd;
|
|
645
647
|
})();
|
|
646
|
-
const largeProbe = await preflightShellLargeFileProbe(command, baseCwd);
|
|
647
|
-
if (largeProbe) return `Error: ${largeProbe.message}`;
|
|
648
648
|
const rawTimeout = typeof args?.timeout === 'number' ? args.timeout : DEFAULT_TIMEOUT_MS;
|
|
649
649
|
const timeoutMs = rawTimeout;
|
|
650
650
|
const effectiveTimeout = Math.min(Math.max(timeoutMs, 1), wmicRewrite?.timeoutMs || MAX_TIMEOUT_MS);
|
|
@@ -21,7 +21,6 @@ import {
|
|
|
21
21
|
import {
|
|
22
22
|
analyzeShellCommandEffects,
|
|
23
23
|
foregroundLongCommandHint,
|
|
24
|
-
preflightShellLargeFileProbe,
|
|
25
24
|
} from './shell-analysis.mjs';
|
|
26
25
|
import {
|
|
27
26
|
cancelBackgroundTask,
|
|
@@ -201,8 +200,6 @@ export async function executeBashTool(args, workDir, options = {}) {
|
|
|
201
200
|
return _execPolicyBlock;
|
|
202
201
|
}
|
|
203
202
|
|
|
204
|
-
const largeProbe = await preflightShellLargeFileProbe(command, bashWorkDir);
|
|
205
|
-
if (largeProbe) return `Error: ${largeProbe.message}`;
|
|
206
203
|
let shellEffects;
|
|
207
204
|
try {
|
|
208
205
|
shellEffects = await analyzeShellCommandEffects(command, bashWorkDir);
|
|
@@ -211,9 +208,14 @@ export async function executeBashTool(args, workDir, options = {}) {
|
|
|
211
208
|
}
|
|
212
209
|
// Keep foreground commands on a long tool-owned timeout. The MCP dispatch
|
|
213
210
|
// layer must not add a shorter fallback ceiling when timeout is omitted.
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
211
|
+
// Claude Code parity (refs/claude-code src/utils/timeouts.ts): default
|
|
212
|
+
// 120 s (2 min), max 600 s (10 min); BASH_DEFAULT_TIMEOUT_MS /
|
|
213
|
+
// BASH_MAX_TIMEOUT_MS env overrides, max floored at default.
|
|
214
|
+
const _envDefaultTimeout = parseInt(process.env.BASH_DEFAULT_TIMEOUT_MS ?? '', 10);
|
|
215
|
+
const DEFAULT_BASH_TIMEOUT_MS = _envDefaultTimeout > 0 ? _envDefaultTimeout : 120_000;
|
|
216
|
+
const DEFAULT_BACKGROUND_BASH_TIMEOUT_MS = DEFAULT_BASH_TIMEOUT_MS;
|
|
217
|
+
const _envMaxTimeout = parseInt(process.env.BASH_MAX_TIMEOUT_MS ?? '', 10);
|
|
218
|
+
const MAX_BASH_TIMEOUT_MS = Math.max(_envMaxTimeout > 0 ? _envMaxTimeout : 600_000, DEFAULT_BASH_TIMEOUT_MS);
|
|
217
219
|
const defaultTimeoutMs = runInBackground
|
|
218
220
|
? DEFAULT_BACKGROUND_BASH_TIMEOUT_MS
|
|
219
221
|
: DEFAULT_BASH_TIMEOUT_MS;
|
|
@@ -12,6 +12,19 @@ import {
|
|
|
12
12
|
executionModeSchemaDescription,
|
|
13
13
|
} from '../../../../shared/background-tasks.mjs';
|
|
14
14
|
|
|
15
|
+
// Shell timeout envelope surfaced in the tool schema. Mirrors Claude Code:
|
|
16
|
+
// default 120 s / max 600 s, BASH_DEFAULT_TIMEOUT_MS / BASH_MAX_TIMEOUT_MS
|
|
17
|
+
// env overrides, max floored at default. Keep in sync with
|
|
18
|
+
// builtin/bash-tool.mjs and bash-session.mjs.
|
|
19
|
+
function _shellDefaultTimeoutMs() {
|
|
20
|
+
const parsed = parseInt(process.env.BASH_DEFAULT_TIMEOUT_MS ?? '', 10);
|
|
21
|
+
return parsed > 0 ? parsed : 120_000;
|
|
22
|
+
}
|
|
23
|
+
function _shellMaxTimeoutMs() {
|
|
24
|
+
const parsed = parseInt(process.env.BASH_MAX_TIMEOUT_MS ?? '', 10);
|
|
25
|
+
return Math.max(parsed > 0 ? parsed : 600_000, _shellDefaultTimeoutMs());
|
|
26
|
+
}
|
|
27
|
+
|
|
15
28
|
export const BUILTIN_TOOLS = [
|
|
16
29
|
{
|
|
17
30
|
name: 'read',
|
|
@@ -59,7 +72,7 @@ export const BUILTIN_TOOLS = [
|
|
|
59
72
|
properties: {
|
|
60
73
|
command: { type: 'string', description: 'Command.' },
|
|
61
74
|
cwd: { type: 'string', description: 'Working directory. Persists across shell calls in a session — omit to reuse the previous command\'s directory (e.g. after cd); pass an absolute path to change it.' },
|
|
62
|
-
timeout: { type: 'number', description:
|
|
75
|
+
timeout: { type: 'number', description: `Timeout ms. Default ${_shellDefaultTimeoutMs()} (${_shellDefaultTimeoutMs() / 60000} min); long-running commands may raise it up to ${_shellMaxTimeoutMs()} (${_shellMaxTimeoutMs() / 60000} min).` },
|
|
63
76
|
merge_stderr: { type: 'boolean', description: 'Merge stderr.' },
|
|
64
77
|
mode: { type: 'string', enum: ['sync', 'async'], description: executionModeSchemaDescription('sync') },
|
|
65
78
|
shell: { type: 'string', enum: ['bash', 'powershell'], description: 'Force shell. Windows defaults to PowerShell; bash = Git Bash/POSIX.' },
|
|
@@ -109,7 +109,7 @@ async function sweepStaleShellJobs(dir) {
|
|
|
109
109
|
});
|
|
110
110
|
await Promise.all([
|
|
111
111
|
...expired.flatMap((jobId) =>
|
|
112
|
-
['.json', '.done', '.exit', '.enforced', '.exit.cmd.sh', '.exit.cmd.ps1', '.stdout.log', '.stderr.log'].map((ext) =>
|
|
112
|
+
['.json', '.done', '.exit', '.enforced', '.exit.cmd.sh', '.exit.cmd.ps1', '.exit.user.ps1', '.stdout.log', '.stderr.log'].map((ext) =>
|
|
113
113
|
fsPromises.unlink(join(dir, jobId + ext)).catch(() => {}),
|
|
114
114
|
),
|
|
115
115
|
),
|
|
@@ -86,10 +86,6 @@ function psSingleQuote(s) {
|
|
|
86
86
|
return `'${String(s).replace(/'/g, "''")}'`;
|
|
87
87
|
}
|
|
88
88
|
|
|
89
|
-
function powerShellEncodedCommand(command) {
|
|
90
|
-
return Buffer.from(String(command || ''), 'utf16le').toString('base64');
|
|
91
|
-
}
|
|
92
|
-
|
|
93
89
|
function isPowerShellShell(shell, shellType) {
|
|
94
90
|
if (shellType === 'powershell') return true;
|
|
95
91
|
const stem = basename(String(shell || '')).toLowerCase().replace(/\.exe$/, '');
|
|
@@ -857,14 +853,24 @@ function startBackgroundPowerShellJob({ command, timeoutMs, workDir, mergeStderr
|
|
|
857
853
|
const exitPath = shellJobExitPath(jobId);
|
|
858
854
|
const donePath = shellJobDonePath(jobId);
|
|
859
855
|
const wrappedTempPath = `${exitPath}.cmd.ps1`;
|
|
860
|
-
|
|
856
|
+
// Stage the USER command as its own .ps1 and run it via `-File` instead of
|
|
857
|
+
// `-EncodedCommand <base64>`: the base64 token landed on the grandchild's
|
|
858
|
+
// visible command line, which is a prime Defender obfuscation signature
|
|
859
|
+
// (same family as the PowhidSubExec false positive). A file path on the
|
|
860
|
+
// command line carries no such signature, and the payload stays opaque to
|
|
861
|
+
// outer-shell quoting exactly as base64 did. UTF-8 BOM so Windows
|
|
862
|
+
// PowerShell 5.1 doesn't misread non-ASCII as ANSI.
|
|
863
|
+
// Trailer mirrors -Command/-EncodedCommand exit semantics (-File alone
|
|
864
|
+
// does NOT propagate the last native command's exit code).
|
|
865
|
+
const innerTempPath = `${exitPath}.user.ps1`;
|
|
866
|
+
const innerScript = `\ufeff${command}\nif ($null -ne $LASTEXITCODE) { exit $LASTEXITCODE }\n`;
|
|
861
867
|
const mergeLiteral = mergeStderr ? '$true' : '$false';
|
|
862
868
|
const wrapper = [
|
|
863
869
|
"$ErrorActionPreference = 'Continue'",
|
|
864
870
|
'[Console]::OutputEncoding=[System.Text.Encoding]::UTF8',
|
|
865
871
|
'$OutputEncoding=[System.Text.Encoding]::UTF8',
|
|
866
872
|
'$exe = (Get-Process -Id $PID).Path',
|
|
867
|
-
`$
|
|
873
|
+
`$innerPath = ${psSingleQuote(innerTempPath)}`,
|
|
868
874
|
`$stdoutPath = ${psSingleQuote(stdoutPath)}`,
|
|
869
875
|
`$stderrPath = ${psSingleQuote(rawStderrPath)}`,
|
|
870
876
|
`$exitPath = ${psSingleQuote(exitPath)}`,
|
|
@@ -873,7 +879,10 @@ function startBackgroundPowerShellJob({ command, timeoutMs, workDir, mergeStderr
|
|
|
873
879
|
`$timeoutMs = ${Math.max(1, Math.floor(timeoutMs || 0))}`,
|
|
874
880
|
'$code = 1',
|
|
875
881
|
'try {',
|
|
876
|
-
|
|
882
|
+
// -ExecutionPolicy Bypass: unlike -EncodedCommand, -File is subject to
|
|
883
|
+
// the execution policy on Windows PowerShell; pwsh on macOS/Linux
|
|
884
|
+
// accepts the parameter as a no-op, so it is safe unconditionally.
|
|
885
|
+
" $argList = @('-NoLogo', '-NoProfile', '-NonInteractive', '-ExecutionPolicy', 'Bypass', '-File', $innerPath)",
|
|
877
886
|
// -WindowStyle is a Windows-only Start-Process parameter; pwsh on
|
|
878
887
|
// macOS/Linux throws "not supported on this platform". Add it only on win32.
|
|
879
888
|
' $spArgs = @{ FilePath = $exe; ArgumentList = $argList; RedirectStandardOutput = $stdoutPath; RedirectStandardError = $stderrPath; PassThru = $true }',
|
|
@@ -912,23 +921,27 @@ function startBackgroundPowerShellJob({ command, timeoutMs, workDir, mergeStderr
|
|
|
912
921
|
'}',
|
|
913
922
|
'try { Set-Content -LiteralPath $exitPath -Value ([string]$code) -NoNewline -Encoding ascii } catch {}',
|
|
914
923
|
'try { Set-Content -LiteralPath $donePath -Value "" -NoNewline -Encoding ascii } catch {}',
|
|
924
|
+
'try { Remove-Item -LiteralPath $innerPath -Force -ErrorAction SilentlyContinue } catch {}',
|
|
915
925
|
'try { Remove-Item -LiteralPath $PSCommandPath -Force -ErrorAction SilentlyContinue } catch {}',
|
|
916
926
|
'exit $code',
|
|
917
927
|
'',
|
|
918
928
|
].join('\n');
|
|
919
929
|
try {
|
|
920
930
|
writeFileSync(wrappedTempPath, wrapper, 'utf-8');
|
|
931
|
+
writeFileSync(innerTempPath, innerScript, 'utf-8');
|
|
921
932
|
} catch (e) {
|
|
922
933
|
return { jobId, kind: 'bash', status: 'failed', error: `failed to stage PowerShell background task: ${e?.message || e}` };
|
|
923
934
|
}
|
|
924
935
|
|
|
925
936
|
const shellStem = basename(String(shell || '')).toLowerCase().replace(/\.exe$/, '');
|
|
926
|
-
// `-WindowStyle Hidden`
|
|
927
|
-
//
|
|
928
|
-
//
|
|
937
|
+
// No `-WindowStyle Hidden` CLI switch: windowsHide:true on the spawn below
|
|
938
|
+
// already gives CREATE_NO_WINDOW, and the visible command-line token trips
|
|
939
|
+
// Defender's hidden-PowerShell dropper signature (PowhidSubExec). The
|
|
940
|
+
// in-wrapper Start-Process WindowStyle=Hidden (above) stays — it lives in
|
|
941
|
+
// the staged .ps1, not on the command line, and is still needed there.
|
|
942
|
+
// `-ExecutionPolicy` only applies to Windows PowerShell; build per-platform.
|
|
929
943
|
const isWin = process.platform === 'win32';
|
|
930
944
|
const wrapperArgs = ['-NoLogo', '-NoProfile', '-NonInteractive'];
|
|
931
|
-
if (isWin) wrapperArgs.push('-WindowStyle', 'Hidden');
|
|
932
945
|
if (isWin && shellStem === 'powershell') wrapperArgs.push('-ExecutionPolicy', 'Bypass');
|
|
933
946
|
wrapperArgs.push('-File', wrappedTempPath);
|
|
934
947
|
// Spawn the staged wrapper directly. detached MUST be false on Windows:
|
|
@@ -27,11 +27,12 @@ function shellTypeFor(shell) {
|
|
|
27
27
|
|
|
28
28
|
function shellSpec(shell, shellType = shellTypeFor(shell)) {
|
|
29
29
|
if (shellType === 'powershell') {
|
|
30
|
-
// `-WindowStyle Hidden`
|
|
31
|
-
// (
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
30
|
+
// Deliberately NO `-WindowStyle Hidden` here: every spawn site passes
|
|
31
|
+
// windowsHide:true (CREATE_NO_WINDOW), which suppresses the console at
|
|
32
|
+
// the OS level without leaving a token on the command line. The CLI
|
|
33
|
+
// switch is redundant AND matches Defender's node→hidden-PowerShell
|
|
34
|
+
// dropper signature (Trojan:Win32/PowhidSubExec false positive).
|
|
35
|
+
const psArgs = ['-NoLogo', '-NoProfile', '-NonInteractive', '-Command'];
|
|
35
36
|
return {
|
|
36
37
|
shell,
|
|
37
38
|
shellArg: '-Command',
|