mixdog 0.9.51 → 0.9.53
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 +5 -3
- package/scripts/abort-recovery-test.mjs +17 -1
- package/scripts/agent-model-liveness-test.mjs +79 -2
- package/scripts/anthropic-admission-retry-integration-test.mjs +119 -0
- package/scripts/anthropic-transport-policy-test.mjs +466 -0
- package/scripts/atomic-lock-tryonce-test.mjs +60 -1
- package/scripts/bench-run.mjs +2 -2
- package/scripts/build-tui.mjs +13 -1
- package/scripts/channel-daemon-smoke.mjs +630 -10
- package/scripts/code-graph-aggregate-cwd-test.mjs +41 -37
- package/scripts/code-graph-disk-hit-test.mjs +11 -0
- package/scripts/code-graph-root-federation-test.mjs +273 -0
- package/scripts/compact-pressure-test.mjs +159 -1
- package/scripts/compact-smoke.mjs +80 -0
- package/scripts/context-mcp-metering-test.mjs +1350 -19
- package/scripts/deferred-tool-loading-test.mjs +17 -0
- package/scripts/desktop-session-bridge-test.mjs +704 -0
- package/scripts/freevar-smoke.mjs +7 -4
- package/scripts/gemini-provider-test.mjs +1053 -0
- package/scripts/interrupted-turn-history-test.mjs +371 -0
- package/scripts/lifecycle-api-test.mjs +137 -0
- package/scripts/max-output-recovery-test.mjs +86 -0
- package/scripts/mcp-grace-deferred-test.mjs +89 -13
- package/scripts/memory-core-input-test.mjs +10 -0
- package/scripts/memory-pg-recovery-test.mjs +59 -0
- package/scripts/openai-oauth-ws-1006-retry-test.mjs +387 -6
- package/scripts/openai-ws-early-settle-test.mjs +40 -0
- package/scripts/parent-abort-link-test.mjs +24 -0
- package/scripts/process-lifecycle-test.mjs +447 -0
- package/scripts/provider-admission-scheduler-test.mjs +582 -0
- package/scripts/provider-contract-test.mjs +525 -0
- package/scripts/provider-toolcall-test.mjs +492 -11
- package/scripts/reactive-compact-persist-smoke.mjs +59 -0
- package/scripts/resource-admission-test.mjs +789 -0
- package/scripts/session-orphan-sweep-test.mjs +27 -1
- package/scripts/shell-jobs-windows-hide-test.mjs +1 -1
- package/scripts/steering-drain-buckets-test.mjs +18 -0
- package/scripts/toolcall-args-test.mjs +14 -6
- package/scripts/tui-transcript-perf-test.mjs +5 -7
- package/src/cli.mjs +15 -2
- package/src/lib/keychain-cjs.cjs +36 -23
- package/src/runtime/agent/orchestrator/agent-runtime/agent-dispatch.mjs +27 -13
- package/src/runtime/agent/orchestrator/agent-runtime/agent-progress-watchdog.mjs +4 -1
- package/src/runtime/agent/orchestrator/agent-runtime/cache-strategy.mjs +7 -8
- package/src/runtime/agent/orchestrator/agent-trace.mjs +33 -9
- package/src/runtime/agent/orchestrator/providers/admission-scheduler.mjs +331 -0
- package/src/runtime/agent/orchestrator/providers/anthropic-effort.mjs +7 -2
- package/src/runtime/agent/orchestrator/providers/anthropic-oauth.mjs +114 -308
- package/src/runtime/agent/orchestrator/providers/anthropic-sse.mjs +31 -21
- package/src/runtime/agent/orchestrator/providers/anthropic.mjs +136 -286
- package/src/runtime/agent/orchestrator/providers/gemini-cache.mjs +42 -5
- package/src/runtime/agent/orchestrator/providers/gemini-schema.mjs +554 -42
- package/src/runtime/agent/orchestrator/providers/gemini-stream.mjs +67 -18
- package/src/runtime/agent/orchestrator/providers/gemini.mjs +84 -150
- package/src/runtime/agent/orchestrator/providers/grok-oauth.mjs +17 -119
- package/src/runtime/agent/orchestrator/providers/lib/anthropic-request-utils.mjs +224 -0
- package/src/runtime/agent/orchestrator/providers/lib/env-utils.mjs +6 -0
- package/src/runtime/agent/orchestrator/providers/lib/gemini-model-catalog.mjs +119 -0
- package/src/runtime/agent/orchestrator/providers/lib/grok-tool-schema.mjs +109 -0
- package/src/runtime/agent/orchestrator/providers/lib/openai-tool-args.mjs +70 -0
- package/src/runtime/agent/orchestrator/providers/model-catalog.mjs +54 -14
- package/src/runtime/agent/orchestrator/providers/openai-compat-presets.mjs +1 -1
- package/src/runtime/agent/orchestrator/providers/openai-compat-stream.mjs +28 -74
- package/src/runtime/agent/orchestrator/providers/openai-compat-xai.mjs +10 -80
- package/src/runtime/agent/orchestrator/providers/openai-compat.mjs +136 -20
- package/src/runtime/agent/orchestrator/providers/openai-oauth-http-sse.mjs +94 -33
- package/src/runtime/agent/orchestrator/providers/openai-oauth-ws.mjs +201 -123
- package/src/runtime/agent/orchestrator/providers/openai-oauth.mjs +37 -28
- package/src/runtime/agent/orchestrator/providers/openai-ws-delta.mjs +10 -10
- package/src/runtime/agent/orchestrator/providers/openai-ws-events.mjs +30 -3
- package/src/runtime/agent/orchestrator/providers/openai-ws-pool.mjs +36 -3
- package/src/runtime/agent/orchestrator/providers/openai-ws-stream.mjs +50 -45
- package/src/runtime/agent/orchestrator/providers/openai-ws.mjs +78 -12
- package/src/runtime/agent/orchestrator/providers/opencode-go.mjs +44 -5
- package/src/runtime/agent/orchestrator/providers/registry.mjs +49 -8
- package/src/runtime/agent/orchestrator/providers/retry-classifier.mjs +229 -111
- package/src/runtime/agent/orchestrator/session/agent-loop.mjs +130 -63
- package/src/runtime/agent/orchestrator/session/compact/engine.mjs +99 -32
- package/src/runtime/agent/orchestrator/session/context-compaction-policy.mjs +170 -0
- package/src/runtime/agent/orchestrator/session/context-utils.mjs +37 -224
- package/src/runtime/agent/orchestrator/session/loop/compact-policy.mjs +11 -17
- package/src/runtime/agent/orchestrator/session/manager/ask-session.mjs +81 -42
- package/src/runtime/agent/orchestrator/session/manager/compaction-runner.mjs +10 -1
- package/src/runtime/agent/orchestrator/session/manager/idle-cleanup.mjs +21 -5
- package/src/runtime/agent/orchestrator/session/manager/message-sanitize.mjs +8 -28
- package/src/runtime/agent/orchestrator/session/manager/prefetch-bridge.mjs +3 -58
- package/src/runtime/agent/orchestrator/session/manager/runtime-liveness.mjs +30 -7
- package/src/runtime/agent/orchestrator/session/manager/session-close.mjs +2 -0
- package/src/runtime/agent/orchestrator/session/manager/session-crud.mjs +10 -1
- package/src/runtime/agent/orchestrator/session/manager/session-lifecycle.mjs +42 -1
- package/src/runtime/agent/orchestrator/session/manager/turn-interruption.mjs +220 -0
- package/src/runtime/agent/orchestrator/session/manager/usage-metrics.mjs +57 -16
- package/src/runtime/agent/orchestrator/session/pre-send-compact.mjs +20 -4
- package/src/runtime/agent/orchestrator/session/save-session-worker.mjs +2 -2
- package/src/runtime/agent/orchestrator/session/send-with-recovery.mjs +12 -1
- package/src/runtime/agent/orchestrator/session/store/paths-heartbeat.mjs +52 -0
- package/src/runtime/agent/orchestrator/session/store/write-guards.mjs +62 -0
- package/src/runtime/agent/orchestrator/session/store-summary-index.mjs +17 -0
- package/src/runtime/agent/orchestrator/session/store.mjs +353 -108
- package/src/runtime/agent/orchestrator/stall-policy.mjs +2 -12
- package/src/runtime/agent/orchestrator/tools/bash-session.mjs +42 -4
- package/src/runtime/agent/orchestrator/tools/builtin/bash-tool.mjs +74 -37
- package/src/runtime/agent/orchestrator/tools/builtin/lib/list-helpers.mjs +46 -0
- package/src/runtime/agent/orchestrator/tools/builtin/lib/search-grep-chunks.mjs +173 -0
- package/src/runtime/agent/orchestrator/tools/builtin/lib/search-input-helpers.mjs +117 -0
- package/src/runtime/agent/orchestrator/tools/builtin/lib/shell-job-insights.mjs +199 -0
- package/src/runtime/agent/orchestrator/tools/builtin/lib/shell-spawn-helpers.mjs +107 -0
- package/src/runtime/agent/orchestrator/tools/builtin/list-tool.mjs +1 -40
- package/src/runtime/agent/orchestrator/tools/builtin/search-tool.mjs +19 -277
- package/src/runtime/agent/orchestrator/tools/builtin/shell-job-process.mjs +223 -2
- package/src/runtime/agent/orchestrator/tools/builtin/shell-jobs.mjs +317 -250
- package/src/runtime/agent/orchestrator/tools/code-graph/build.mjs +176 -21
- package/src/runtime/agent/orchestrator/tools/code-graph/disk-cache.mjs +14 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/dispatch.mjs +108 -2
- package/src/runtime/agent/orchestrator/tools/code-graph/trusted-roots.mjs +93 -0
- package/src/runtime/agent/orchestrator/tools/code-graph-prewarm-worker.mjs +12 -3
- package/src/runtime/agent/orchestrator/tools/lib/shell-spawn-retry.mjs +67 -0
- package/src/runtime/agent/orchestrator/tools/shell-command.mjs +125 -85
- package/src/runtime/channels/backends/discord-gateway.mjs +1 -1
- package/src/runtime/channels/backends/discord.mjs +6 -6
- package/src/runtime/channels/lib/config.mjs +15 -2
- package/src/runtime/channels/lib/memory-client.mjs +20 -3
- package/src/runtime/channels/lib/owned-runtime.mjs +19 -12
- package/src/runtime/channels/lib/status-snapshot.mjs +9 -0
- package/src/runtime/channels/lib/tool-dispatch.mjs +9 -5
- package/src/runtime/channels/lib/worker-main.mjs +16 -5
- package/src/runtime/memory/index.mjs +46 -202
- package/src/runtime/memory/lib/memory-action-handlers.mjs +2 -53
- package/src/runtime/memory/lib/memory-daemon-lifecycle.mjs +115 -0
- package/src/runtime/memory/lib/memory-port-advertiser.mjs +105 -0
- package/src/runtime/memory/lib/pg/adapter.mjs +84 -10
- package/src/runtime/memory/lib/pg/process.mjs +91 -47
- package/src/runtime/memory/lib/pg/supervisor.mjs +50 -17
- package/src/runtime/memory/lib/query-handlers.mjs +2 -122
- package/src/runtime/memory/lib/query-maintenance-handlers.mjs +126 -0
- package/src/runtime/memory/lib/tool-call-handler.mjs +57 -0
- package/src/runtime/search/lib/http-fetch.mjs +1 -1
- package/src/runtime/shared/atomic-file.mjs +28 -150
- package/src/runtime/shared/config.mjs +58 -13
- package/src/runtime/shared/llm/cost.mjs +14 -4
- package/src/runtime/shared/memory-snapshot.mjs +236 -0
- package/src/runtime/shared/process-lifecycle.mjs +436 -0
- package/src/runtime/shared/process-shutdown.mjs +33 -4
- package/src/runtime/shared/resource-admission.mjs +364 -0
- package/src/runtime/shared/staged-child-result.mjs +19 -0
- package/src/session-runtime/channel-config-api.mjs +7 -7
- package/src/session-runtime/config-lifecycle.mjs +20 -17
- package/src/session-runtime/context-status.mjs +38 -27
- package/src/session-runtime/cwd-plugins.mjs +9 -7
- package/src/session-runtime/env.mjs +1 -2
- package/src/session-runtime/hitch-profile.mjs +45 -0
- package/src/session-runtime/lifecycle-api.mjs +60 -13
- package/src/session-runtime/mcp-glue.mjs +6 -11
- package/src/session-runtime/provider-init-key.mjs +17 -0
- package/src/session-runtime/provider-request-tools.mjs +72 -0
- package/src/session-runtime/runtime-core.mjs +49 -105
- package/src/session-runtime/runtime-paths.mjs +20 -0
- package/src/session-runtime/runtime-tool-routing.mjs +55 -0
- package/src/session-runtime/session-turn-api.mjs +5 -4
- package/src/session-runtime/tool-catalog-data.mjs +51 -0
- package/src/session-runtime/tool-catalog.mjs +390 -104
- package/src/standalone/agent-tool/spawn-preset.mjs +73 -0
- package/src/standalone/agent-tool/worker-rows.mjs +93 -0
- package/src/standalone/agent-tool.mjs +29 -200
- package/src/standalone/channel-admin.mjs +29 -0
- package/src/standalone/channel-daemon-client.mjs +200 -38
- package/src/standalone/channel-daemon-transport.mjs +136 -9
- package/src/standalone/channel-worker.mjs +79 -12
- package/src/tui/App.jsx +11 -5
- package/src/tui/dist/index.mjs +275 -343
- package/src/tui/engine/session-api-ext.mjs +50 -6
- package/src/tui/engine/session-api.mjs +1 -1
- package/src/tui/engine/turn.mjs +10 -6
- package/src/tui/engine.mjs +13 -4
- package/src/tui/index.jsx +5 -1
- package/src/tui/lib/voice-setup.mjs +21 -5
- package/scripts/_devtools-stub.mjs +0 -1
- package/src/runtime/lib/keychain-cjs.cjs +0 -288
|
@@ -33,15 +33,18 @@ import { randomUUID } from 'node:crypto';
|
|
|
33
33
|
import * as nodeUtil from 'node:util';
|
|
34
34
|
import { getPluginData } from '../config.mjs';
|
|
35
35
|
import { startChildGuardian } from '../../../shared/child-guardian.mjs';
|
|
36
|
+
import { resourceAdmission } from '../../../shared/resource-admission.mjs';
|
|
36
37
|
// Runtime-only import (used inside execShellCommand's auto-background
|
|
37
38
|
// transition). shell-jobs.mjs imports stripAnsi from this module, so this is
|
|
38
39
|
// a static cycle — safe because neither binding is touched at module-eval
|
|
39
40
|
// time, only when the respective functions actually run.
|
|
40
41
|
import { adoptForegroundShellJob, killShellJob } from './builtin/shell-jobs.mjs';
|
|
42
|
+
import { trackProcessTreeQuiescence } from './builtin/shell-job-process.mjs';
|
|
41
43
|
import {
|
|
42
44
|
_maybeEncodePowerShellCommand,
|
|
43
45
|
extractPowerShellCommandInner,
|
|
44
46
|
} from './shell-powershell.mjs';
|
|
47
|
+
import { spawnShellWithRetry as _spawnShellWithRetry } from './lib/shell-spawn-retry.mjs';
|
|
45
48
|
|
|
46
49
|
export {
|
|
47
50
|
_maybeEncodePowerShellCommand,
|
|
@@ -445,12 +448,6 @@ async function _execPolicyBlockMessage(command) {
|
|
|
445
448
|
// EPERM backoff). Logged with each failed spawn so a Defender-induced storm
|
|
446
449
|
// is reconstructable: activeSpawnCount > 1 means concurrent spawns were
|
|
447
450
|
// racing the AV scan when the failure hit.
|
|
448
|
-
let _activeShellSpawns = 0;
|
|
449
|
-
|
|
450
|
-
function _isPowerShellSpawn(shell, shellArg) {
|
|
451
|
-
return /pwsh|powershell/i.test(String(shell || '')) || shellArg === '-Command';
|
|
452
|
-
}
|
|
453
|
-
|
|
454
451
|
// Windows Defender intermittently fails node→PowerShell spawns with EPERM
|
|
455
452
|
// while it scans the child image (see shell-runtime.mjs Trojan false-positive
|
|
456
453
|
// note). The failure is at spawn() time — before any stdio/side effect — so a
|
|
@@ -458,71 +455,6 @@ function _isPowerShellSpawn(shell, shellArg) {
|
|
|
458
455
|
// Retry ONLY on EPERM/win32/powershell; everything else throws on first
|
|
459
456
|
// failure. Backoff 100/300/700ms caps added latency at ~1.1s. Every failed
|
|
460
457
|
// attempt logs one diagnostic line for later reconstruction.
|
|
461
|
-
async function _spawnShellWithRetry({ shell, argv, spawnOptions, shellArg, cwd }) {
|
|
462
|
-
const _delays = [100, 300, 700];
|
|
463
|
-
const _isPowerShell = _isPowerShellSpawn(shell, shellArg);
|
|
464
|
-
_activeShellSpawns++;
|
|
465
|
-
try {
|
|
466
|
-
let attempt = 0;
|
|
467
|
-
for (;;) {
|
|
468
|
-
try {
|
|
469
|
-
const child = spawn(shell, argv, spawnOptions);
|
|
470
|
-
// spawn() reports ENOENT/EACCES on nextTick. Keep a guard listener
|
|
471
|
-
// attached from the same synchronous frame as spawn(), and do not
|
|
472
|
-
// resolve this helper until the child emits `spawn`. The caller adopts
|
|
473
|
-
// the guard atomically (new listener first, guard removal second), so
|
|
474
|
-
// there is never an unhandled-error window across either await.
|
|
475
|
-
let bufferedError = null;
|
|
476
|
-
const guardError = (err) => { bufferedError = bufferedError || err; };
|
|
477
|
-
child.on('error', guardError);
|
|
478
|
-
try {
|
|
479
|
-
await new Promise((resolveSpawn, rejectSpawn) => {
|
|
480
|
-
const onSpawn = () => {
|
|
481
|
-
child.removeListener('error', onError);
|
|
482
|
-
resolveSpawn();
|
|
483
|
-
};
|
|
484
|
-
const onError = (err) => {
|
|
485
|
-
child.removeListener('spawn', onSpawn);
|
|
486
|
-
rejectSpawn(err);
|
|
487
|
-
};
|
|
488
|
-
child.once('spawn', onSpawn);
|
|
489
|
-
child.once('error', onError);
|
|
490
|
-
});
|
|
491
|
-
} catch (err) {
|
|
492
|
-
child.removeListener('error', guardError);
|
|
493
|
-
throw err;
|
|
494
|
-
}
|
|
495
|
-
return {
|
|
496
|
-
child,
|
|
497
|
-
adoptErrorHandler(handler) {
|
|
498
|
-
child.on('error', handler);
|
|
499
|
-
child.removeListener('error', guardError);
|
|
500
|
-
if (bufferedError) handler(bufferedError);
|
|
501
|
-
},
|
|
502
|
-
};
|
|
503
|
-
} catch (err) {
|
|
504
|
-
try {
|
|
505
|
-
console.error('[shell-spawn-retry] ' + JSON.stringify({
|
|
506
|
-
code: (err && err.code) || null,
|
|
507
|
-
syscall: (err && err.syscall) || null,
|
|
508
|
-
shell,
|
|
509
|
-
cwd,
|
|
510
|
-
activeSpawnCount: _activeShellSpawns,
|
|
511
|
-
}));
|
|
512
|
-
} catch { /* logging must never mask the spawn error */ }
|
|
513
|
-
const _canRetry = err && err.code === 'EPERM'
|
|
514
|
-
&& process.platform === 'win32'
|
|
515
|
-
&& _isPowerShell
|
|
516
|
-
&& attempt < _delays.length;
|
|
517
|
-
if (!_canRetry) throw err;
|
|
518
|
-
await new Promise((r) => setTimeout(r, _delays[attempt++]));
|
|
519
|
-
}
|
|
520
|
-
}
|
|
521
|
-
} finally {
|
|
522
|
-
_activeShellSpawns--;
|
|
523
|
-
}
|
|
524
|
-
}
|
|
525
|
-
|
|
526
458
|
export function execShellCommand({
|
|
527
459
|
shell,
|
|
528
460
|
shellArg,
|
|
@@ -537,8 +469,16 @@ export function execShellCommand({
|
|
|
537
469
|
clientHostPid,
|
|
538
470
|
backgroundOnTimeout,
|
|
539
471
|
promotedTimeoutMs = 0,
|
|
472
|
+
admission = resourceAdmission,
|
|
540
473
|
}) {
|
|
541
474
|
return new Promise(async (resolve) => {
|
|
475
|
+
let resultResolved = false;
|
|
476
|
+
const resolveResult = (result) => {
|
|
477
|
+
if (resultResolved) return false;
|
|
478
|
+
resultResolved = true;
|
|
479
|
+
resolve(result);
|
|
480
|
+
return true;
|
|
481
|
+
};
|
|
542
482
|
const taskId = `shell_${randomUUID().slice(0, 8)}`;
|
|
543
483
|
const taskOutput = new TaskOutput(taskId);
|
|
544
484
|
let timedOut = false;
|
|
@@ -553,6 +493,49 @@ export function execShellCommand({
|
|
|
553
493
|
let timer = null;
|
|
554
494
|
let abortHandler = null;
|
|
555
495
|
let partialOutput = false;
|
|
496
|
+
let resourceLease = null;
|
|
497
|
+
let resourceLeaseSettlement = null;
|
|
498
|
+
const releaseResourceLease = async () => {
|
|
499
|
+
if (!resourceLease) return null;
|
|
500
|
+
const lease = resourceLease;
|
|
501
|
+
resourceLease = null;
|
|
502
|
+
try {
|
|
503
|
+
await lease.release();
|
|
504
|
+
return null;
|
|
505
|
+
} catch (error) {
|
|
506
|
+
return error;
|
|
507
|
+
}
|
|
508
|
+
};
|
|
509
|
+
const releaseResourceLeaseWhenTreeQuiescent = (pid, { waitForRootExit = false } = {}) => {
|
|
510
|
+
if (resourceLeaseSettlement) return resourceLeaseSettlement;
|
|
511
|
+
if (!resourceLease) return { pending: false, promise: Promise.resolve() };
|
|
512
|
+
const lease = resourceLease;
|
|
513
|
+
resourceLease = null;
|
|
514
|
+
const cleanup = Promise.withResolvers();
|
|
515
|
+
const tracker = trackProcessTreeQuiescence(pid, () => {
|
|
516
|
+
try {
|
|
517
|
+
Promise.resolve(lease.release()).then(
|
|
518
|
+
() => cleanup.resolve({ error: null }),
|
|
519
|
+
(error) => cleanup.resolve({ error }),
|
|
520
|
+
);
|
|
521
|
+
} catch (error) {
|
|
522
|
+
cleanup.resolve({ error });
|
|
523
|
+
}
|
|
524
|
+
}, { waitForRootExit });
|
|
525
|
+
resourceLeaseSettlement = {
|
|
526
|
+
lease,
|
|
527
|
+
tracker,
|
|
528
|
+
get pending() { return tracker.pending; },
|
|
529
|
+
promise: cleanup.promise,
|
|
530
|
+
};
|
|
531
|
+
return resourceLeaseSettlement;
|
|
532
|
+
};
|
|
533
|
+
const detachAbortHandler = () => {
|
|
534
|
+
if (abortSignal && abortHandler) {
|
|
535
|
+
try { abortSignal.removeEventListener('abort', abortHandler); } catch {}
|
|
536
|
+
abortHandler = null;
|
|
537
|
+
}
|
|
538
|
+
};
|
|
556
539
|
// MCP live-progress: throttled "running Ns, M lines" emits while the
|
|
557
540
|
// foreground command runs. Inert (never armed) when onProgress is null.
|
|
558
541
|
const _hasProgress = typeof onProgress === 'function';
|
|
@@ -568,6 +551,7 @@ export function execShellCommand({
|
|
|
568
551
|
// the child's lifecycle is owned by the shell-jobs registry. Mutually
|
|
569
552
|
// exclusive with `settled`: whichever transition wins first wins for good.
|
|
570
553
|
let autoBackgrounded = false;
|
|
554
|
+
let autoBackgroundJobId = null;
|
|
571
555
|
let autoBgTimer = null;
|
|
572
556
|
// Treekill + force-settle deadline. treeKill alone leaves settle()
|
|
573
557
|
// pending on 'close'/'exit'; on Windows a taskkill miss or a grandchild
|
|
@@ -597,9 +581,14 @@ export function execShellCommand({
|
|
|
597
581
|
|
|
598
582
|
let child;
|
|
599
583
|
try {
|
|
584
|
+
resourceLease = await admission.acquire('shell', {
|
|
585
|
+
signal: abortSignal || null,
|
|
586
|
+
label: String(command || '').slice(0, 120),
|
|
587
|
+
});
|
|
600
588
|
const _policyErr = await _execPolicyBlockMessage(command);
|
|
601
589
|
if (_policyErr) {
|
|
602
|
-
|
|
590
|
+
await releaseResourceLease();
|
|
591
|
+
resolveResult(
|
|
603
592
|
new ExecResult({
|
|
604
593
|
stdout: '',
|
|
605
594
|
stderr: _policyErr,
|
|
@@ -655,18 +644,28 @@ export function execShellCommand({
|
|
|
655
644
|
childGroupPid: child.pid,
|
|
656
645
|
label: 'shell-command',
|
|
657
646
|
});
|
|
647
|
+
// Windows has no process-group handle. Begin observing while the owned
|
|
648
|
+
// root still exists so descendants can be proven before root exit.
|
|
649
|
+
releaseResourceLeaseWhenTreeQuiescent(child.pid, { waitForRootExit: true });
|
|
658
650
|
} catch (err) {
|
|
659
|
-
|
|
651
|
+
const cleanupError = await releaseResourceLease();
|
|
652
|
+
const spawnText = String((err && err.message) || err);
|
|
653
|
+
const cleanupText = cleanupError
|
|
654
|
+
? `${spawnText}; resource cleanup failed: ${cleanupError?.message || cleanupError}`
|
|
655
|
+
: spawnText;
|
|
656
|
+
resolveResult(
|
|
660
657
|
new ExecResult({
|
|
661
658
|
stdout: '',
|
|
662
|
-
stderr:
|
|
659
|
+
stderr: cleanupText,
|
|
663
660
|
exitCode: 1,
|
|
664
661
|
signal: null,
|
|
665
662
|
timedOut: false,
|
|
666
663
|
killed: false,
|
|
667
664
|
taskId,
|
|
668
665
|
failurePhase: 'tool',
|
|
669
|
-
failureReason: '
|
|
666
|
+
failureReason: err?.code === 'ERESOURCEPRESSURE' || err?.code === 'ERESOURCEQUEUEFULL'
|
|
667
|
+
? 'resource pressure'
|
|
668
|
+
: 'spawn failed',
|
|
670
669
|
}),
|
|
671
670
|
);
|
|
672
671
|
return;
|
|
@@ -712,11 +711,7 @@ export function execShellCommand({
|
|
|
712
711
|
clearTimeout(autoBgTimer);
|
|
713
712
|
autoBgTimer = null;
|
|
714
713
|
}
|
|
715
|
-
|
|
716
|
-
try {
|
|
717
|
-
abortSignal.removeEventListener('abort', abortHandler);
|
|
718
|
-
} catch {}
|
|
719
|
-
}
|
|
714
|
+
detachAbortHandler();
|
|
720
715
|
// getStdout/getStderr can throw on a spilled-file read failure (EBADF
|
|
721
716
|
// after unlink race, EACCES). Without this catch the rejection bubbles
|
|
722
717
|
// up and leaves the outer settle promise unresolved, hanging the call.
|
|
@@ -741,7 +736,12 @@ export function execShellCommand({
|
|
|
741
736
|
} else {
|
|
742
737
|
taskOutput.closeFds();
|
|
743
738
|
}
|
|
744
|
-
|
|
739
|
+
const leaseSettlement = resourceLeaseSettlement
|
|
740
|
+
|| releaseResourceLeaseWhenTreeQuiescent(child.pid);
|
|
741
|
+
leaseSettlement.tracker?.rootExited?.();
|
|
742
|
+
await leaseSettlement.tracker?.afterRootExitCheck?.();
|
|
743
|
+
if (!leaseSettlement.pending) await leaseSettlement.promise;
|
|
744
|
+
resolveResult(
|
|
745
745
|
new ExecResult({
|
|
746
746
|
stdout,
|
|
747
747
|
stderr,
|
|
@@ -863,6 +863,8 @@ export function execShellCommand({
|
|
|
863
863
|
}
|
|
864
864
|
return;
|
|
865
865
|
}
|
|
866
|
+
const promotedLease = resourceLeaseSettlement?.lease || resourceLease;
|
|
867
|
+
child.once('close', () => { resourceLeaseSettlement?.tracker?.rootExited?.(); });
|
|
866
868
|
// Wire the lifecycle: on close, write the exit-code file FIRST then
|
|
867
869
|
// touch donePath STRICTLY AFTER — the exact ordering refreshShellJob()
|
|
868
870
|
// gates completion on (donePath visible ⇒ exit file fully flushed).
|
|
@@ -881,6 +883,7 @@ export function execShellCommand({
|
|
|
881
883
|
try { stderr = await taskOutput.getStderr(); }
|
|
882
884
|
catch (err) { taskOutput.writeError = taskOutput.writeError || err; }
|
|
883
885
|
const jobId = job ? job.jobId : null;
|
|
886
|
+
autoBackgroundJobId = jobId;
|
|
884
887
|
// Re-check after the awaited capture reads: cancellation can race after
|
|
885
888
|
// adoption commits. Never report that cancelled process as a successful
|
|
886
889
|
// still-running background task.
|
|
@@ -889,7 +892,8 @@ export function execShellCommand({
|
|
|
889
892
|
killCause = 'cancellation';
|
|
890
893
|
try { killShellJob(jobId); } catch {}
|
|
891
894
|
try { treeKill(child); } catch {}
|
|
892
|
-
|
|
895
|
+
await promotedLease?.detachDependency?.();
|
|
896
|
+
resolveResult(new ExecResult({
|
|
893
897
|
stdout,
|
|
894
898
|
stderr,
|
|
895
899
|
exitCode: null,
|
|
@@ -908,11 +912,20 @@ export function execShellCommand({
|
|
|
908
912
|
}));
|
|
909
913
|
return;
|
|
910
914
|
}
|
|
915
|
+
// Promotion changes a scoped dependency into detached lifetime work.
|
|
916
|
+
// Re-admit the continuing parent through the normal bounded queue before
|
|
917
|
+
// returning control; if the child exits first, its close release supplies
|
|
918
|
+
// the capacity that completes this restoration.
|
|
919
|
+
await promotedLease?.detachDependency?.();
|
|
920
|
+
// The adopted job now owns cancellation through task control. Retaining
|
|
921
|
+
// the foreground caller's signal listener would keep the completed tool
|
|
922
|
+
// frame alive and could later kill an unrelated, already-returned job.
|
|
923
|
+
detachAbortHandler();
|
|
911
924
|
const secs = Math.max(0, Math.round((Date.now() - _startMs) / 1000));
|
|
912
925
|
const _verb = reason === 'timeout'
|
|
913
926
|
? `moved to background at timeout after ${secs}s`
|
|
914
927
|
: `auto-backgrounded after ${secs}s`;
|
|
915
|
-
|
|
928
|
+
resolveResult(
|
|
916
929
|
new ExecResult({
|
|
917
930
|
stdout,
|
|
918
931
|
stderr,
|
|
@@ -936,6 +949,33 @@ export function execShellCommand({
|
|
|
936
949
|
}),
|
|
937
950
|
);
|
|
938
951
|
};
|
|
952
|
+
const fireAutoBackground = (options) => {
|
|
953
|
+
void _autoBackground(options).catch((error) => {
|
|
954
|
+
if (resultResolved) return;
|
|
955
|
+
settled = true;
|
|
956
|
+
autoBackgrounded = true;
|
|
957
|
+
killed = true;
|
|
958
|
+
killCause = 'resource-cleanup-error';
|
|
959
|
+
detachAbortHandler();
|
|
960
|
+
try { if (autoBackgroundJobId) killShellJob(autoBackgroundJobId); } catch {}
|
|
961
|
+
try { treeKill(child); } catch {}
|
|
962
|
+
resolveResult(new ExecResult({
|
|
963
|
+
stdout: '',
|
|
964
|
+
stderr: `resource cleanup failed during background promotion: ${error?.message || error}`,
|
|
965
|
+
exitCode: 1,
|
|
966
|
+
signal: child?.signalCode || null,
|
|
967
|
+
timedOut: false,
|
|
968
|
+
killed: true,
|
|
969
|
+
killCause,
|
|
970
|
+
taskId,
|
|
971
|
+
partialOutput: true,
|
|
972
|
+
outputCaptureError: taskOutput.writeError,
|
|
973
|
+
failurePhase: 'tool',
|
|
974
|
+
failureReason: 'resource cleanup failed',
|
|
975
|
+
backgrounded: false,
|
|
976
|
+
}));
|
|
977
|
+
});
|
|
978
|
+
};
|
|
939
979
|
|
|
940
980
|
if (timeoutMs > 0) {
|
|
941
981
|
timer = setTimeout(() => {
|
|
@@ -953,7 +993,7 @@ export function execShellCommand({
|
|
|
953
993
|
child.exitCode == null &&
|
|
954
994
|
child.signalCode == null
|
|
955
995
|
) {
|
|
956
|
-
|
|
996
|
+
fireAutoBackground({ reason: 'timeout' });
|
|
957
997
|
return;
|
|
958
998
|
}
|
|
959
999
|
timedOut = true;
|
|
@@ -985,7 +1025,7 @@ export function execShellCommand({
|
|
|
985
1025
|
!_isBackground &&
|
|
986
1026
|
(timeoutMs <= 0 || autoBackgroundMs < timeoutMs)
|
|
987
1027
|
) {
|
|
988
|
-
autoBgTimer = setTimeout(() => {
|
|
1028
|
+
autoBgTimer = setTimeout(() => { fireAutoBackground(); }, autoBackgroundMs);
|
|
989
1029
|
if (autoBgTimer.unref) autoBgTimer.unref();
|
|
990
1030
|
}
|
|
991
1031
|
|
|
@@ -18,7 +18,7 @@ import { join, sep } from "path";
|
|
|
18
18
|
import { createHash } from "crypto";
|
|
19
19
|
import { chunk, formatForDiscord, MAX_DISCORD_MESSAGE } from "../lib/format.mjs";
|
|
20
20
|
import { withConfigLock } from "../lib/config-lock.mjs";
|
|
21
|
-
import { readSection,
|
|
21
|
+
import { readSection, updateSectionAsync } from "../../shared/config.mjs";
|
|
22
22
|
import { normalizeAccess, safeAttName } from "./discord-access.mjs";
|
|
23
23
|
import { MAX_ATTACHMENT_BYTES, downloadSingleAttachment } from "./discord-attachments.mjs";
|
|
24
24
|
import * as gateway from "./discord-gateway.mjs";
|
|
@@ -473,7 +473,7 @@ class DiscordBackend {
|
|
|
473
473
|
return this.initialAccess;
|
|
474
474
|
}
|
|
475
475
|
}
|
|
476
|
-
persistAccessFromMainChannel() {
|
|
476
|
+
async persistAccessFromMainChannel() {
|
|
477
477
|
if (this.isStatic || !this.configFile) return;
|
|
478
478
|
try {
|
|
479
479
|
const id = this.mainChannelId;
|
|
@@ -482,7 +482,7 @@ class DiscordBackend {
|
|
|
482
482
|
const access = normalizeAccess(parsed.access);
|
|
483
483
|
if (!(id in access.channels)) {
|
|
484
484
|
access.channels[id] = { requireMention: false, allowFrom: [] };
|
|
485
|
-
this.saveAccess(access);
|
|
485
|
+
await this.saveAccess(access);
|
|
486
486
|
}
|
|
487
487
|
} catch (err) {
|
|
488
488
|
process.stderr.write(`mixdog discord: persistAccessFromMainChannel failed: ${err}\n`);
|
|
@@ -499,10 +499,10 @@ class DiscordBackend {
|
|
|
499
499
|
}
|
|
500
500
|
return a;
|
|
501
501
|
}
|
|
502
|
-
saveAccess(a) {
|
|
502
|
+
async saveAccess(a) {
|
|
503
503
|
if (this.isStatic) return;
|
|
504
504
|
if (!this.configFile) return;
|
|
505
|
-
return withConfigLock(() => {
|
|
505
|
+
return withConfigLock(async () => {
|
|
506
506
|
mkdirSync(this.stateDir, { recursive: true, mode: 448 });
|
|
507
507
|
const access = {
|
|
508
508
|
dmPolicy: a.dmPolicy,
|
|
@@ -513,7 +513,7 @@ class DiscordBackend {
|
|
|
513
513
|
...a.replyToMode ? { replyToMode: a.replyToMode } : {},
|
|
514
514
|
...a.textChunkLimit ? { textChunkLimit: a.textChunkLimit } : {}
|
|
515
515
|
};
|
|
516
|
-
|
|
516
|
+
await updateSectionAsync("channels", (channels) => ({
|
|
517
517
|
...channels,
|
|
518
518
|
access
|
|
519
519
|
}));
|
|
@@ -2,7 +2,16 @@ import { readFileSync, mkdirSync } from "fs";
|
|
|
2
2
|
import { join } from "path";
|
|
3
3
|
import { DiscordBackend } from "../backends/discord.mjs";
|
|
4
4
|
import { TelegramBackend } from "../backends/telegram.mjs";
|
|
5
|
-
import {
|
|
5
|
+
import {
|
|
6
|
+
readSection,
|
|
7
|
+
updateSection,
|
|
8
|
+
CONFIG_PATH as MIXDOG_CONFIG_PATH,
|
|
9
|
+
SECRET_ACCOUNTS,
|
|
10
|
+
getDiscordToken,
|
|
11
|
+
getTelegramToken,
|
|
12
|
+
diagnoseDiscordTokenValue,
|
|
13
|
+
invalidateSecretReadCache,
|
|
14
|
+
} from "../../shared/config.mjs";
|
|
6
15
|
import { listSchedules } from "../../shared/schedules-db.mjs";
|
|
7
16
|
import { resolvePluginData } from "../../shared/plugin-paths.mjs";
|
|
8
17
|
const DATA_DIR = resolvePluginData();
|
|
@@ -46,8 +55,12 @@ function resolveChannelId(raw = {}, backend = "discord") {
|
|
|
46
55
|
return "";
|
|
47
56
|
}
|
|
48
57
|
|
|
49
|
-
async function loadConfig() {
|
|
58
|
+
async function loadConfig({ freshSecrets = false } = {}) {
|
|
50
59
|
try {
|
|
60
|
+
if (freshSecrets) {
|
|
61
|
+
invalidateSecretReadCache(SECRET_ACCOUNTS.discordToken);
|
|
62
|
+
invalidateSecretReadCache(SECRET_ACCOUNTS.telegramToken);
|
|
63
|
+
}
|
|
51
64
|
let raw = readSection("channels");
|
|
52
65
|
raw = raw && typeof raw === "object" ? raw : {};
|
|
53
66
|
// Schedules are the PG `scheduler.schedules` table (single source of
|
|
@@ -218,6 +218,16 @@ function atomicWrite(targetPath, contents) {
|
|
|
218
218
|
// site, so no drain/replay format or TTL semantics change.
|
|
219
219
|
const _dedupeIndex = new Map()
|
|
220
220
|
let _dedupeIndexSeeded = false
|
|
221
|
+
function dropDedupeIndexForPath(bufferPath) {
|
|
222
|
+
for (const [key, indexedPath] of _dedupeIndex) {
|
|
223
|
+
if (indexedPath === bufferPath) _dedupeIndex.delete(key)
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
function replaceDedupeIndexPath(previousPath, nextPath) {
|
|
227
|
+
for (const [key, indexedPath] of _dedupeIndex) {
|
|
228
|
+
if (indexedPath === previousPath) _dedupeIndex.set(key, nextPath)
|
|
229
|
+
}
|
|
230
|
+
}
|
|
221
231
|
function seedDedupeIndex(kind) {
|
|
222
232
|
if (_dedupeIndexSeeded) return
|
|
223
233
|
_dedupeIndexSeeded = true
|
|
@@ -266,9 +276,11 @@ function bufferToDisk(kind, payload, { dedupeKey = null } = {}) {
|
|
|
266
276
|
// leaves the file in place and returns false so the caller skips it this pass.
|
|
267
277
|
function moveToDead(name, reason) {
|
|
268
278
|
process.stderr.write(`[memory-client] quarantining ${name} to dead/ (${reason})\n`)
|
|
279
|
+
const bufferPath = path.join(BUFFER_DIR, name)
|
|
269
280
|
try {
|
|
270
281
|
fs.mkdirSync(DEAD_DIR, { recursive: true })
|
|
271
|
-
fs.renameSync(
|
|
282
|
+
fs.renameSync(bufferPath, path.join(DEAD_DIR, name))
|
|
283
|
+
dropDedupeIndexForPath(bufferPath)
|
|
272
284
|
return true
|
|
273
285
|
} catch (e) {
|
|
274
286
|
process.stderr.write(`[memory-client] quarantine of ${name} failed (${e.message}) — leaving in place\n`)
|
|
@@ -355,7 +367,10 @@ export async function drainBuffer() {
|
|
|
355
367
|
// throwOnError: non-2xx status or an {error} body REJECTS, so the
|
|
356
368
|
// file is kept/aged for retry instead of unlinked (HIGH: data loss).
|
|
357
369
|
await memoryFetch('POST', endpoint, payload, 10_000, { throwOnError: true })
|
|
358
|
-
try {
|
|
370
|
+
try {
|
|
371
|
+
fs.unlinkSync(bufferPath)
|
|
372
|
+
dropDedupeIndexForPath(bufferPath)
|
|
373
|
+
} catch {}
|
|
359
374
|
drained++
|
|
360
375
|
} catch (e) {
|
|
361
376
|
const attempts = parseRetry(name) + 1
|
|
@@ -375,7 +390,9 @@ export async function drainBuffer() {
|
|
|
375
390
|
// THIS pass so it can't block the oldest-first queue forever — the
|
|
376
391
|
// next drain re-reads its (unchanged) .rN and retries.
|
|
377
392
|
try {
|
|
378
|
-
|
|
393
|
+
const nextPath = path.join(BUFFER_DIR, retryName(name, attempts))
|
|
394
|
+
fs.renameSync(bufferPath, nextPath)
|
|
395
|
+
replaceDedupeIndexPath(bufferPath, nextPath)
|
|
379
396
|
} catch {
|
|
380
397
|
// Rename lock/EPERM: don't break the whole pass on a file we
|
|
381
398
|
// couldn't even age — skip it and CONTINUE so later buffered
|
|
@@ -410,7 +410,9 @@ async function refreshBridgeOwnership(options = {}) {
|
|
|
410
410
|
async function reloadRuntimeConfig() {
|
|
411
411
|
const previousBackend = getBackend();
|
|
412
412
|
const previousBackendName = previousBackend?.name || "";
|
|
413
|
-
|
|
413
|
+
// File-watch/tool-triggered reloads must bypass the short keychain hit cache:
|
|
414
|
+
// another process may have just saved or rotated the channel credential.
|
|
415
|
+
setConfig(await loadConfig({ freshSecrets: true }));
|
|
414
416
|
scheduler.reloadConfig(
|
|
415
417
|
getConfig().nonInteractive ?? [],
|
|
416
418
|
getConfig().interactive ?? [],
|
|
@@ -419,7 +421,10 @@ async function reloadRuntimeConfig() {
|
|
|
419
421
|
{ restart: getBridgeRuntimeConnected() }
|
|
420
422
|
);
|
|
421
423
|
const nextBackend = createBackend(getConfig());
|
|
422
|
-
const
|
|
424
|
+
const backendTypeChanged = (nextBackend?.name || "") !== previousBackendName;
|
|
425
|
+
const credentialsChanged = !backendTypeChanged
|
|
426
|
+
&& String(nextBackend?.token || "") !== String(previousBackend?.token || "");
|
|
427
|
+
const backendChanged = backendTypeChanged || credentialsChanged;
|
|
423
428
|
if (backendChanged) {
|
|
424
429
|
const shouldRestart = getBridgeRuntimeConnected() || bridgeRuntimeStarting;
|
|
425
430
|
if (shouldRestart) await stopOwnedRuntime("getBackend() getConfig() changed");
|
|
@@ -439,19 +444,21 @@ async function reloadRuntimeConfig() {
|
|
|
439
444
|
// backendChanged — same-getBackend() reloads keep their binding untouched.
|
|
440
445
|
// (active-instance is cleared by stopOwnedRuntime on the restart path; we
|
|
441
446
|
// don't re-advertise here to avoid resurrecting a just-cleared entry.)
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
state
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
447
|
+
if (backendTypeChanged) {
|
|
448
|
+
try { forwarder.stopWatch(); } catch {}
|
|
449
|
+
forwarder.channelId = "";
|
|
450
|
+
forwarder.transcriptPath = "";
|
|
451
|
+
try {
|
|
452
|
+
statusState.update((state) => {
|
|
453
|
+
state.channelId = "";
|
|
454
|
+
state.transcriptPath = "";
|
|
455
|
+
});
|
|
456
|
+
} catch {}
|
|
457
|
+
}
|
|
451
458
|
// stopOwnedRuntime above tore the owned runtime down; a same-session reload
|
|
452
459
|
// reconnects the NEW backend here. The in-flight start (if any) has already
|
|
453
460
|
// settled above, so this start is not dropped by the in-flight guard.
|
|
454
|
-
if (shouldRestart) refreshBridgeOwnershipSafe({ restoreBinding:
|
|
461
|
+
if (shouldRestart) refreshBridgeOwnershipSafe({ restoreBinding: !backendTypeChanged });
|
|
455
462
|
} else if (nextBackend !== previousBackend) {
|
|
456
463
|
try { await nextBackend.disconnect?.(); } catch {}
|
|
457
464
|
}
|
|
@@ -35,6 +35,7 @@ function stableSnapshotJson(snapshot) {
|
|
|
35
35
|
// Key: channelId Value: { label, latestSeenId, unseenCount }
|
|
36
36
|
// No persistence across restarts — clean start is fine for v1.
|
|
37
37
|
const _discordUnread = new Map();
|
|
38
|
+
const DISCORD_UNREAD_MAX_CHANNELS = 500;
|
|
38
39
|
|
|
39
40
|
/**
|
|
40
41
|
* Called whenever the backend observes messages for a channelId.
|
|
@@ -63,6 +64,14 @@ export function recordFetchedMessages(channelId, channelLabel, messages, options
|
|
|
63
64
|
}
|
|
64
65
|
// First call: zero unread (baseline, not retroactive).
|
|
65
66
|
|
|
67
|
+
// Retain the most recently observed channels only. Channel ids originate from
|
|
68
|
+
// inbound traffic and ad-hoc fetches, so an unbounded process-wide registry
|
|
69
|
+
// would otherwise grow for the worker lifetime.
|
|
70
|
+
if (_discordUnread.has(channelId)) {
|
|
71
|
+
_discordUnread.delete(channelId);
|
|
72
|
+
} else if (_discordUnread.size >= DISCORD_UNREAD_MAX_CHANNELS) {
|
|
73
|
+
_discordUnread.delete(_discordUnread.keys().next().value);
|
|
74
|
+
}
|
|
66
75
|
_discordUnread.set(channelId, {
|
|
67
76
|
label: channelLabel ?? channelId,
|
|
68
77
|
latestSeenId: newLatestId,
|
|
@@ -82,14 +82,18 @@ function createToolDispatch({
|
|
|
82
82
|
}
|
|
83
83
|
case "reload_config": {
|
|
84
84
|
await reloadRuntimeConfig();
|
|
85
|
-
// Extend reload to the agent
|
|
86
|
-
// hot-reload on the same call
|
|
87
|
-
// import channels, so this stays acyclic and tolerant of load order).
|
|
85
|
+
// Extend reload to the refactored agent runtime so providers/presets/
|
|
86
|
+
// maintenance hot-reload on the same call.
|
|
88
87
|
let agentReloadMsg = "";
|
|
89
88
|
if (process.env.MIXDOG_STANDALONE !== '1') {
|
|
90
89
|
try {
|
|
91
|
-
const {
|
|
92
|
-
|
|
90
|
+
const [{ loadConfig }, { initProviders, refreshCatalogs }] = await Promise.all([
|
|
91
|
+
import("../../agent/orchestrator/config.mjs"),
|
|
92
|
+
import("../../agent/orchestrator/providers/registry.mjs"),
|
|
93
|
+
]);
|
|
94
|
+
const agentConfig = loadConfig();
|
|
95
|
+
await initProviders(agentConfig.providers || {});
|
|
96
|
+
await refreshCatalogs();
|
|
93
97
|
agentReloadMsg = ", agent providers/presets/maintenance";
|
|
94
98
|
} catch (err) {
|
|
95
99
|
process.stderr.write(`[reload_config] agent reload failed: ${err?.message || String(err)}\n`);
|
|
@@ -461,7 +461,19 @@ function wireWebhookHandlers() {
|
|
|
461
461
|
// final event is forwarded to the Lead via the same channel-notify
|
|
462
462
|
// path used by schedule & event-queue (injectAndRecord). Silent
|
|
463
463
|
// lifecycle pings keep routing only to Discord.
|
|
464
|
-
const
|
|
464
|
+
const [{ createStandaloneAgent }, cfgMod, reg, mgr] = await Promise.all([
|
|
465
|
+
import("../../../standalone/agent-tool.mjs"),
|
|
466
|
+
import("../../agent/orchestrator/config.mjs"),
|
|
467
|
+
import("../../agent/orchestrator/providers/registry.mjs"),
|
|
468
|
+
import("../../agent/orchestrator/session/manager.mjs"),
|
|
469
|
+
]);
|
|
470
|
+
const agentTool = createStandaloneAgent({
|
|
471
|
+
cfgMod,
|
|
472
|
+
reg,
|
|
473
|
+
mgr,
|
|
474
|
+
dataDir: cfgMod.getPluginData(),
|
|
475
|
+
cwd,
|
|
476
|
+
});
|
|
465
477
|
const channelId = resolveWebhookChannelId(context?.channel);
|
|
466
478
|
const endpoint = context?.endpoint || "unknown";
|
|
467
479
|
const event = context?.event || null;
|
|
@@ -508,10 +520,9 @@ function wireWebhookHandlers() {
|
|
|
508
520
|
// Falls back to the session entry position; never the plugin CACHE root.
|
|
509
521
|
const ownerPid = getActiveOwnerPid(readActiveInstance());
|
|
510
522
|
const ownerCwd = (ownerPid && readLastSessionCwd(ownerPid)) || captureOriginalUserCwd();
|
|
511
|
-
return
|
|
512
|
-
"
|
|
513
|
-
{
|
|
514
|
-
{ notifyFn },
|
|
523
|
+
return agentTool.execute(
|
|
524
|
+
{ type: "spawn", agent: role, preset, prompt, cwd: cwd || ownerCwd },
|
|
525
|
+
{ callerCwd: cwd || ownerCwd, invocationSource: "webhook", notifyFn },
|
|
515
526
|
);
|
|
516
527
|
});
|
|
517
528
|
}
|