visionclaw 0.1.219-dev.codex-issue-2805-worker-runtime.2 → 0.1.219-dev.codex-fix-2941-activity-foreground.1
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/dist/agent/api-proxy.d.ts +16 -0
- package/dist/agent/api-proxy.d.ts.map +1 -1
- package/dist/agent/api-proxy.js +8 -9
- package/dist/agent/api-proxy.js.map +1 -1
- package/dist/agent/browser-launcher.d.ts +0 -3
- package/dist/agent/browser-launcher.d.ts.map +1 -1
- package/dist/agent/browser-launcher.js +43 -101
- package/dist/agent/browser-launcher.js.map +1 -1
- package/dist/agent/coding-conventions.d.ts +3 -2
- package/dist/agent/coding-conventions.d.ts.map +1 -1
- package/dist/agent/coding-conventions.js +8 -7
- package/dist/agent/coding-conventions.js.map +1 -1
- package/dist/agent/command-handlers.d.ts.map +1 -1
- package/dist/agent/command-handlers.js +16 -4
- package/dist/agent/command-handlers.js.map +1 -1
- package/dist/agent/context-action-restart-fallback.d.ts.map +1 -1
- package/dist/agent/context-action-restart-fallback.js +3 -9
- package/dist/agent/context-action-restart-fallback.js.map +1 -1
- package/dist/agent/context-refresh.d.ts.map +1 -1
- package/dist/agent/context-refresh.js +7 -0
- package/dist/agent/context-refresh.js.map +1 -1
- package/dist/agent/context.d.ts.map +1 -1
- package/dist/agent/context.js +8 -6
- package/dist/agent/context.js.map +1 -1
- package/dist/agent/engine-fallback.d.ts +35 -0
- package/dist/agent/engine-fallback.d.ts.map +1 -1
- package/dist/agent/engine-fallback.js +3 -4
- package/dist/agent/engine-fallback.js.map +1 -1
- package/dist/agent/engines/browser-tool-policy.d.ts +1 -12
- package/dist/agent/engines/browser-tool-policy.d.ts.map +1 -1
- package/dist/agent/engines/browser-tool-policy.js +2 -17
- package/dist/agent/engines/browser-tool-policy.js.map +1 -1
- package/dist/agent/engines/claude/session.d.ts +0 -1
- package/dist/agent/engines/claude/session.d.ts.map +1 -1
- package/dist/agent/engines/claude/session.js +36 -21
- package/dist/agent/engines/claude/session.js.map +1 -1
- package/dist/agent/engines/pi/tools.d.ts.map +1 -1
- package/dist/agent/engines/pi/tools.js +10 -2
- package/dist/agent/engines/pi/tools.js.map +1 -1
- package/dist/agent/engines/pi-coding/session.d.ts +0 -3
- package/dist/agent/engines/pi-coding/session.d.ts.map +1 -1
- package/dist/agent/engines/pi-coding/session.js +22 -19
- package/dist/agent/engines/pi-coding/session.js.map +1 -1
- package/dist/agent/engines/session-types.d.ts +0 -6
- package/dist/agent/engines/session-types.d.ts.map +1 -1
- package/dist/agent/external-context/runtime.d.ts.map +1 -1
- package/dist/agent/external-context/runtime.js +14 -15
- package/dist/agent/external-context/runtime.js.map +1 -1
- package/dist/agent/fast-responder.d.ts +20 -4
- package/dist/agent/fast-responder.d.ts.map +1 -1
- package/dist/agent/fast-responder.js +39 -9
- package/dist/agent/fast-responder.js.map +1 -1
- package/dist/agent/interrupt-handler.d.ts +18 -4
- package/dist/agent/interrupt-handler.d.ts.map +1 -1
- package/dist/agent/interrupt-handler.js +284 -49
- package/dist/agent/interrupt-handler.js.map +1 -1
- package/dist/agent/lab-run-system-prompt.d.ts.map +1 -1
- package/dist/agent/lab-run-system-prompt.js +0 -3
- package/dist/agent/lab-run-system-prompt.js.map +1 -1
- package/dist/agent/lifecycle-trace.d.ts.map +1 -1
- package/dist/agent/lifecycle-trace.js +5 -6
- package/dist/agent/lifecycle-trace.js.map +1 -1
- package/dist/agent/loop.d.ts +1 -4
- package/dist/agent/loop.d.ts.map +1 -1
- package/dist/agent/loop.js +129 -105
- package/dist/agent/loop.js.map +1 -1
- package/dist/agent/memory-protocol.d.ts +2 -2
- package/dist/agent/memory-protocol.d.ts.map +1 -1
- package/dist/agent/memory-protocol.js +15 -3
- package/dist/agent/memory-protocol.js.map +1 -1
- package/dist/agent/memory-store.d.ts.map +1 -1
- package/dist/agent/memory-store.js +1 -11
- package/dist/agent/memory-store.js.map +1 -1
- package/dist/agent/model-provider.d.ts +0 -2
- package/dist/agent/model-provider.d.ts.map +1 -1
- package/dist/agent/model-provider.js +25 -31
- package/dist/agent/model-provider.js.map +1 -1
- package/dist/agent/product-identity.d.ts +4 -5
- package/dist/agent/product-identity.d.ts.map +1 -1
- package/dist/agent/product-identity.js +6 -7
- package/dist/agent/product-identity.js.map +1 -1
- package/dist/agent/public-activity.d.ts +1 -1
- package/dist/agent/public-activity.d.ts.map +1 -1
- package/dist/agent/public-activity.js.map +1 -1
- package/dist/agent/reply-watchdog.d.ts +3 -5
- package/dist/agent/reply-watchdog.d.ts.map +1 -1
- package/dist/agent/reply-watchdog.js +4 -6
- package/dist/agent/reply-watchdog.js.map +1 -1
- package/dist/agent/session-manager.d.ts +0 -5
- package/dist/agent/session-manager.d.ts.map +1 -1
- package/dist/agent/session-manager.js +0 -7
- package/dist/agent/session-manager.js.map +1 -1
- package/dist/agent/state.d.ts +0 -7
- package/dist/agent/state.d.ts.map +1 -1
- package/dist/agent/state.js +11 -21
- package/dist/agent/state.js.map +1 -1
- package/dist/agent/stream-handler.d.ts.map +1 -1
- package/dist/agent/stream-handler.js +17 -0
- package/dist/agent/stream-handler.js.map +1 -1
- package/dist/agent/system-prompt.d.ts.map +1 -1
- package/dist/agent/system-prompt.js +6 -10
- package/dist/agent/system-prompt.js.map +1 -1
- package/dist/agent/transcript/transcript-indexer.d.ts +28 -0
- package/dist/agent/transcript/transcript-indexer.d.ts.map +1 -1
- package/dist/agent/transcript/transcript-indexer.js +57 -58
- package/dist/agent/transcript/transcript-indexer.js.map +1 -1
- package/dist/agent/usage/usage-report-queue.d.ts.map +1 -1
- package/dist/agent/usage/usage-report-queue.js +52 -53
- package/dist/agent/usage/usage-report-queue.js.map +1 -1
- package/dist/agent/wake-cycle-runner.d.ts +8 -7
- package/dist/agent/wake-cycle-runner.d.ts.map +1 -1
- package/dist/agent/wake-cycle-runner.js +66 -105
- package/dist/agent/wake-cycle-runner.js.map +1 -1
- package/dist/builtin-skills/owner-manual/SKILL.md +2 -2
- package/dist/calendar/occurrence-ownership.d.ts.map +1 -1
- package/dist/calendar/occurrence-ownership.js +5 -6
- package/dist/calendar/occurrence-ownership.js.map +1 -1
- package/dist/calendar/scheduler.d.ts.map +1 -1
- package/dist/calendar/scheduler.js +3 -4
- package/dist/calendar/scheduler.js.map +1 -1
- package/dist/channels/interface.d.ts +2 -8
- package/dist/channels/interface.d.ts.map +1 -1
- package/dist/channels/interface.js +1 -8
- package/dist/channels/interface.js.map +1 -1
- package/dist/channels/manager.d.ts.map +1 -1
- package/dist/channels/manager.js +0 -2
- package/dist/channels/manager.js.map +1 -1
- package/dist/channels/qq-status.d.ts.map +1 -1
- package/dist/channels/qq-status.js +1 -0
- package/dist/channels/qq-status.js.map +1 -1
- package/dist/channels/queue.d.ts.map +1 -1
- package/dist/channels/queue.js +0 -8
- package/dist/channels/queue.js.map +1 -1
- package/dist/channels/telegram-status.d.ts.map +1 -1
- package/dist/channels/telegram-status.js +2 -0
- package/dist/channels/telegram-status.js.map +1 -1
- package/dist/channels/tui.d.ts +2 -2
- package/dist/channels/tui.d.ts.map +1 -1
- package/dist/channels/tui.js +2 -7
- package/dist/channels/tui.js.map +1 -1
- package/dist/config/index.d.ts.map +1 -1
- package/dist/config/index.js +15 -16
- package/dist/config/index.js.map +1 -1
- package/dist/config/types.d.ts +8 -16
- package/dist/config/types.d.ts.map +1 -1
- package/dist/config/types.js +24 -13
- package/dist/config/types.js.map +1 -1
- package/dist/email/email-route-store.d.ts.map +1 -1
- package/dist/email/email-route-store.js +5 -6
- package/dist/email/email-route-store.js.map +1 -1
- package/dist/i18n/messages.d.ts +0 -12
- package/dist/i18n/messages.d.ts.map +1 -1
- package/dist/i18n/messages.js +9 -21
- package/dist/i18n/messages.js.map +1 -1
- package/dist/logger.d.ts +0 -2
- package/dist/logger.d.ts.map +1 -1
- package/dist/logger.js +1 -19
- package/dist/logger.js.map +1 -1
- package/dist/threads/active-threads-store.d.ts.map +1 -1
- package/dist/threads/active-threads-store.js +27 -8
- package/dist/threads/active-threads-store.js.map +1 -1
- package/dist/threads/execution-host.d.ts.map +1 -1
- package/dist/threads/execution-host.js +0 -1
- package/dist/threads/execution-host.js.map +1 -1
- package/dist/threads/execution-scheduler.d.ts +0 -4
- package/dist/threads/execution-scheduler.d.ts.map +1 -1
- package/dist/threads/execution-scheduler.js +2 -38
- package/dist/threads/execution-scheduler.js.map +1 -1
- package/dist/threads/execution-store.d.ts +5 -20
- package/dist/threads/execution-store.d.ts.map +1 -1
- package/dist/threads/execution-store.js +8 -45
- package/dist/threads/execution-store.js.map +1 -1
- package/dist/threads/foreground-task.d.ts +6 -10
- package/dist/threads/foreground-task.d.ts.map +1 -1
- package/dist/threads/foreground-task.js +26 -34
- package/dist/threads/foreground-task.js.map +1 -1
- package/dist/threads/foreman-client.d.ts.map +1 -1
- package/dist/threads/foreman-client.js +1 -3
- package/dist/threads/foreman-client.js.map +1 -1
- package/dist/threads/foreman-execution.d.ts +12 -12
- package/dist/threads/foreman-execution.d.ts.map +1 -1
- package/dist/threads/foreman-execution.js +19 -30
- package/dist/threads/foreman-execution.js.map +1 -1
- package/dist/threads/foreman-host.d.ts +2 -17
- package/dist/threads/foreman-host.d.ts.map +1 -1
- package/dist/threads/foreman-host.js +59 -423
- package/dist/threads/foreman-host.js.map +1 -1
- package/dist/threads/foreman-protocol.d.ts +12 -109
- package/dist/threads/foreman-protocol.d.ts.map +1 -1
- package/dist/threads/foreman-protocol.js +15 -37
- package/dist/threads/foreman-protocol.js.map +1 -1
- package/dist/threads/foreman-store.d.ts +9 -36
- package/dist/threads/foreman-store.d.ts.map +1 -1
- package/dist/threads/foreman-store.js +19 -63
- package/dist/threads/foreman-store.js.map +1 -1
- package/dist/threads/generic-execution.d.ts +2 -2
- package/dist/threads/generic-execution.d.ts.map +1 -1
- package/dist/threads/generic-execution.js +6 -9
- package/dist/threads/generic-execution.js.map +1 -1
- package/dist/threads/ipc.d.ts +0 -3
- package/dist/threads/ipc.d.ts.map +1 -1
- package/dist/threads/ipc.js.map +1 -1
- package/dist/threads/relay-adapter.d.ts +2 -0
- package/dist/threads/relay-adapter.d.ts.map +1 -1
- package/dist/threads/relay-adapter.js +7 -1
- package/dist/threads/relay-adapter.js.map +1 -1
- package/dist/threads/supervisor.d.ts +51 -21
- package/dist/threads/supervisor.d.ts.map +1 -1
- package/dist/threads/supervisor.js +449 -160
- package/dist/threads/supervisor.js.map +1 -1
- package/dist/threads/task-contracts.d.ts +194 -0
- package/dist/threads/task-contracts.d.ts.map +1 -0
- package/dist/threads/task-contracts.js +82 -0
- package/dist/threads/task-contracts.js.map +1 -0
- package/dist/threads/task-sidecar.d.ts +145 -0
- package/dist/threads/task-sidecar.d.ts.map +1 -0
- package/dist/threads/task-sidecar.js +311 -0
- package/dist/threads/task-sidecar.js.map +1 -0
- package/dist/threads/task-store.d.ts +172 -0
- package/dist/threads/task-store.d.ts.map +1 -0
- package/dist/threads/task-store.js +599 -0
- package/dist/threads/task-store.js.map +1 -0
- package/dist/threads/thread-activity-controller.d.ts +7 -0
- package/dist/threads/thread-activity-controller.d.ts.map +1 -1
- package/dist/threads/thread-activity-controller.js +52 -2
- package/dist/threads/thread-activity-controller.js.map +1 -1
- package/dist/threads/thread-task-context.d.ts +40 -0
- package/dist/threads/thread-task-context.d.ts.map +1 -0
- package/dist/threads/thread-task-context.js +160 -0
- package/dist/threads/thread-task-context.js.map +1 -0
- package/dist/threads/thread-task-controller.d.ts +122 -0
- package/dist/threads/thread-task-controller.d.ts.map +1 -0
- package/dist/threads/thread-task-controller.js +276 -0
- package/dist/threads/thread-task-controller.js.map +1 -0
- package/dist/threads/thread-task-runtime.d.ts +350 -0
- package/dist/threads/thread-task-runtime.d.ts.map +1 -0
- package/dist/threads/thread-task-runtime.js +896 -0
- package/dist/threads/thread-task-runtime.js.map +1 -0
- package/dist/threads/worker-entry.d.ts.map +1 -1
- package/dist/threads/worker-entry.js +28 -29
- package/dist/threads/worker-entry.js.map +1 -1
- package/dist/threads/worker-execution-runtime.d.ts +2 -3
- package/dist/threads/worker-execution-runtime.d.ts.map +1 -1
- package/dist/threads/worker-execution-runtime.js +2 -5
- package/dist/threads/worker-execution-runtime.js.map +1 -1
- package/dist/tools/delegation.d.ts +2 -71
- package/dist/tools/delegation.d.ts.map +1 -1
- package/dist/tools/delegation.js +28 -62
- package/dist/tools/delegation.js.map +1 -1
- package/dist/tools/email.d.ts +1 -1
- package/dist/tools/execution-control.d.ts +0 -5
- package/dist/tools/execution-control.d.ts.map +1 -1
- package/dist/tools/execution-control.js +2 -9
- package/dist/tools/execution-control.js.map +1 -1
- package/dist/tools/index.d.ts.map +1 -1
- package/dist/tools/index.js +6 -2
- package/dist/tools/index.js.map +1 -1
- package/dist/tools/mcp-manager.d.ts +1 -1
- package/dist/tools/memory.d.ts +1 -1
- package/dist/tools/notify.d.ts +0 -2
- package/dist/tools/notify.d.ts.map +1 -1
- package/dist/tools/notify.js +5 -21
- package/dist/tools/notify.js.map +1 -1
- package/dist/tools/skill.d.ts +1 -1
- package/dist/tools/thread-tasks.d.ts +23 -0
- package/dist/tools/thread-tasks.d.ts.map +1 -0
- package/dist/tools/thread-tasks.js +122 -0
- package/dist/tools/thread-tasks.js.map +1 -0
- package/dist/tools/totp.d.ts +1 -1
- package/dist/tui/request-store.d.ts +1 -1
- package/dist/tui/request-store.d.ts.map +1 -1
- package/dist/tui/request-store.js +2 -2
- package/dist/tui/request-store.js.map +1 -1
- package/dist/tui/server.d.ts.map +1 -1
- package/dist/tui/server.js +2 -17
- package/dist/tui/server.js.map +1 -1
- package/dist/utils/restart.d.ts.map +1 -1
- package/dist/utils/restart.js +0 -6
- package/dist/utils/restart.js.map +1 -1
- package/dist-agent/bundle.cjs +2 -2
- package/package.json +1 -1
- package/dist/agent/runtime-context.d.ts +0 -27
- package/dist/agent/runtime-context.d.ts.map +0 -1
- package/dist/agent/runtime-context.js +0 -60
- package/dist/agent/runtime-context.js.map +0 -1
- package/dist/agent/runtime-disclosure-policy.d.ts +0 -6
- package/dist/agent/runtime-disclosure-policy.d.ts.map +0 -1
- package/dist/agent/runtime-disclosure-policy.js +0 -16
- package/dist/agent/runtime-disclosure-policy.js.map +0 -1
- package/dist/conversations/lazy-runtime.d.ts +0 -5
- package/dist/conversations/lazy-runtime.d.ts.map +0 -1
- package/dist/conversations/lazy-runtime.js +0 -12
- package/dist/conversations/lazy-runtime.js.map +0 -1
- package/dist/conversations/runtime.d.ts +0 -17
- package/dist/conversations/runtime.d.ts.map +0 -1
- package/dist/conversations/runtime.js +0 -92
- package/dist/conversations/runtime.js.map +0 -1
- package/dist/conversations/scheduler.d.ts +0 -57
- package/dist/conversations/scheduler.d.ts.map +0 -1
- package/dist/conversations/scheduler.js +0 -175
- package/dist/conversations/scheduler.js.map +0 -1
- package/dist/threads/foreman-business.d.ts +0 -9
- package/dist/threads/foreman-business.d.ts.map +0 -1
- package/dist/threads/foreman-business.js +0 -117
- package/dist/threads/foreman-business.js.map +0 -1
- package/dist/threads/foreman-delivery.d.ts +0 -5
- package/dist/threads/foreman-delivery.d.ts.map +0 -1
- package/dist/threads/foreman-delivery.js +0 -21
- package/dist/threads/foreman-delivery.js.map +0 -1
- package/dist/threads/task-workspace.d.ts +0 -8
- package/dist/threads/task-workspace.d.ts.map +0 -1
- package/dist/threads/task-workspace.js +0 -52
- package/dist/threads/task-workspace.js.map +0 -1
|
@@ -1,7 +1,17 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
4
|
-
*
|
|
2
|
+
* ThreadSupervisor: orchestrates worker processes for non-default business threads.
|
|
3
|
+
*
|
|
4
|
+
* Responsibilities:
|
|
5
|
+
* - Spawn worker processes on demand (child_process.fork)
|
|
6
|
+
* - Route messages to workers by threadId
|
|
7
|
+
* - Relay worker replies back to real channels
|
|
8
|
+
* - Enforce concurrency limits (maxConcurrentThreads)
|
|
9
|
+
* - Idle timeout & graceful worker shutdown
|
|
10
|
+
* - Crash detection & auto-restart
|
|
11
|
+
* - Force global commands (/restart, /upgrade, /subscribe, …) to the default thread
|
|
12
|
+
* - Engine switch (/engine): update thread.json + restart worker
|
|
13
|
+
* - Worker admission pause for the main process's nightly maintenance
|
|
14
|
+
* - Graceful supervisor shutdown: notify all workers
|
|
5
15
|
*/
|
|
6
16
|
import { fork } from "node:child_process";
|
|
7
17
|
import { randomUUID } from "node:crypto";
|
|
@@ -12,6 +22,8 @@ import { PUBLIC_ACTIVITY_HISTORY_LIMIT } from "../agent/public-activity.js";
|
|
|
12
22
|
import { isDeliveryTarget } from "../channels/delivery-target.js";
|
|
13
23
|
import { DEFAULT_THREAD_ID, isEmailChannel, normalizeChannelName } from "../channels/interface.js";
|
|
14
24
|
import { MessageQueue } from "../channels/queue.js";
|
|
25
|
+
import { isThreadTasksEnabled } from "../config/types.js";
|
|
26
|
+
import { taskStorePathForThread, threadHasRunnableTasks } from "./task-store.js";
|
|
15
27
|
import { logger } from "../logger.js";
|
|
16
28
|
import { isOwnerInitiatedMessage } from "../agent/owner-initiated-eligibility.js";
|
|
17
29
|
import { getProfile, getSharedProfileDir, getThreadDir, loadOwnerConfig } from "../config/index.js";
|
|
@@ -23,9 +35,6 @@ import { ensureThreadApplication, formatThreadEngineLabel, readThreadMeta, setTh
|
|
|
23
35
|
import { getLlmRouting } from "../agent/model-provider.js";
|
|
24
36
|
import { ExecutionStore, isTerminalExecutionState } from "./execution-store.js";
|
|
25
37
|
import { ExecutionScheduler } from "./execution-scheduler.js";
|
|
26
|
-
import { ConversationScheduler } from "../conversations/scheduler.js";
|
|
27
|
-
import { createLazyConversationRuntime } from "../conversations/lazy-runtime.js";
|
|
28
|
-
import { bindRuntimeContext } from "../agent/runtime-context.js";
|
|
29
38
|
import { EXECUTION_VERIFY_KEYS_ENV, createExecutionAuthority, verifyKeysOf, } from "./execution-authority.js";
|
|
30
39
|
import { EXECUTOR_GROUP_REGISTRY_FILE, executorGroupStatus, killExecutorGroupAndWait, reapExitedExecutorGroup, signalExecutorGroup, stopRegisteredGroups, } from "./process-group.js";
|
|
31
40
|
import { ExecutorRefusedError } from "./executor-exit.js";
|
|
@@ -45,7 +54,7 @@ import { AgentFeedbackOutbox } from "../agent/feedback-outbox.js";
|
|
|
45
54
|
import { serializeBrowserRuntimeResolution, serializeDesktopRuntimeResolution, WORKER_BROWSER_RUNTIME_ENV, WORKER_DESKTOP_RUNTIME_ENV, } from "./worker-runtime-resolution.js";
|
|
46
55
|
import { isGlobalCommand } from "./global-commands.js";
|
|
47
56
|
import { getClaudeTranscriptDirForProfileDir } from "../utils/claude-transcripts.js";
|
|
48
|
-
import {
|
|
57
|
+
import { MAX_PARALLEL_FOREMAN_TASKS } from "./foreman-protocol.js";
|
|
49
58
|
export { isGlobalCommand } from "./global-commands.js";
|
|
50
59
|
const __filename = fileURLToPath(import.meta.url);
|
|
51
60
|
const __dirname = path.dirname(__filename);
|
|
@@ -108,8 +117,6 @@ export function parseEngineCommand(text) {
|
|
|
108
117
|
return null;
|
|
109
118
|
}
|
|
110
119
|
export class ThreadSupervisor {
|
|
111
|
-
conversations;
|
|
112
|
-
foregroundDisabled;
|
|
113
120
|
channelManager;
|
|
114
121
|
messageQueue;
|
|
115
122
|
maxConcurrentThreads;
|
|
@@ -120,6 +127,7 @@ export class ThreadSupervisor {
|
|
|
120
127
|
workerEnv;
|
|
121
128
|
desktopRuntimeResolution;
|
|
122
129
|
browserRuntimeResolution;
|
|
130
|
+
crashRestartDelayMs;
|
|
123
131
|
threadQueueFactory;
|
|
124
132
|
onFeedback;
|
|
125
133
|
onBusyChange;
|
|
@@ -137,18 +145,23 @@ export class ThreadSupervisor {
|
|
|
137
145
|
workers = new Map();
|
|
138
146
|
/** Main-process handles for the durable queue.db owned by each thread. */
|
|
139
147
|
threadQueues = new Map();
|
|
148
|
+
/** Crash counts must outlive WorkerState objects, which are replaced on respawn. */
|
|
149
|
+
crashCounts = new Map();
|
|
150
|
+
/** Threads waiting for their bounded crash-backoff timer. */
|
|
151
|
+
restartTimers = new Map();
|
|
152
|
+
/** Startup failures / exhausted crash budgets stay dormant until new work arrives. */
|
|
153
|
+
recoveryExhaustedThreads = new Set();
|
|
140
154
|
/** True once shutdownAll() ran — no new workers may be spawned afterwards. */
|
|
141
155
|
stopped = false;
|
|
142
156
|
maintenancePaused = false;
|
|
157
|
+
maintenanceMessages = [];
|
|
143
158
|
/** The one execution the maintenance owner submitted under its pause. */
|
|
144
159
|
maintenanceExecutionId = null;
|
|
145
160
|
onMaintenanceInput;
|
|
146
161
|
handleForemanCommand;
|
|
147
|
-
handleForemanBusinessRead;
|
|
148
162
|
executionStore;
|
|
149
163
|
executionScheduler;
|
|
150
164
|
constructor(options) {
|
|
151
|
-
this.foregroundDisabled = options.conversationFactory === null;
|
|
152
165
|
this.channelManager = options.channelManager;
|
|
153
166
|
this.messageQueue = options.messageQueue;
|
|
154
167
|
this.maxConcurrentThreads = options.maxConcurrentThreads;
|
|
@@ -159,6 +172,7 @@ export class ThreadSupervisor {
|
|
|
159
172
|
this.workerEnv = options.workerEnv;
|
|
160
173
|
this.desktopRuntimeResolution = options.desktopRuntimeResolution;
|
|
161
174
|
this.browserRuntimeResolution = options.browserRuntimeResolution;
|
|
175
|
+
this.crashRestartDelayMs = options.crashRestartDelayMs ?? 2000;
|
|
162
176
|
this.executionAuthority = options.executionAuthority ?? createExecutionAuthority();
|
|
163
177
|
this.workerStopGraceMs = options.workerStopGraceMs ?? 10_000;
|
|
164
178
|
this.executorGroups = options.executorGroups ?? {
|
|
@@ -172,33 +186,11 @@ export class ThreadSupervisor {
|
|
|
172
186
|
this.onBusyChange = options.onBusyChange;
|
|
173
187
|
this.onMaintenanceInput = options.onMaintenanceInput;
|
|
174
188
|
this.handleForemanCommand = options.handleForemanCommand;
|
|
175
|
-
this.handleForemanBusinessRead = options.handleForemanBusinessRead;
|
|
176
189
|
this.threadQueueFactory = options.threadQueueFactory ?? ((threadId, recoverInFlight) => {
|
|
177
190
|
const threadDir = getThreadDir(threadId);
|
|
178
191
|
fs.mkdirSync(threadDir, { recursive: true });
|
|
179
192
|
return new MessageQueue(path.join(threadDir, "queue.db"), { recoverInFlight });
|
|
180
193
|
});
|
|
181
|
-
// Foreground conversation runtimes never use the background worker transport.
|
|
182
|
-
const conversationCommand = this.handleForemanCommand;
|
|
183
|
-
this.conversations = !this.foregroundDisabled
|
|
184
|
-
? new ConversationScheduler({
|
|
185
|
-
create: options.conversationFactory ?? ((start) => createLazyConversationRuntime({
|
|
186
|
-
...start,
|
|
187
|
-
config: this.config,
|
|
188
|
-
foremanClient: conversationCommand && supportsTaskRuntime()
|
|
189
|
-
? { delegationEnabled: isForemanRuntime(this.config), request: bindRuntimeContext((command) => conversationCommand(start.conversationId, command)) }
|
|
190
|
-
: null,
|
|
191
|
-
desktopRuntimeResolution: this.desktopRuntimeResolution,
|
|
192
|
-
browserRuntimeResolution: this.browserRuntimeResolution,
|
|
193
|
-
})),
|
|
194
|
-
queue: (id) => this.getThreadQueue(id, false),
|
|
195
|
-
emit: (id, event, reply) => this.handleConversationEvent(id, event, reply),
|
|
196
|
-
awareness: (id) => this.getConversationTaskSnapshot(id),
|
|
197
|
-
onBusyChange: this.onBusyChange,
|
|
198
|
-
maxConcurrentTurns: this.config.workers?.foregroundConcurrency ?? 2,
|
|
199
|
-
idleTtlMs: this.idleTimeoutMs,
|
|
200
|
-
})
|
|
201
|
-
: null;
|
|
202
194
|
this.executionStore = options.executionStore === undefined
|
|
203
195
|
? new ExecutionStore()
|
|
204
196
|
: options.executionStore;
|
|
@@ -270,9 +262,7 @@ export class ThreadSupervisor {
|
|
|
270
262
|
* execution runs, is stopping, or waits in the queue.
|
|
271
263
|
*/
|
|
272
264
|
tryPauseForMaintenance() {
|
|
273
|
-
if (this.stopped || this.maintenancePaused)
|
|
274
|
-
return null;
|
|
275
|
-
if (this.conversations?.hasBusyConversations())
|
|
265
|
+
if (this.stopped || this.maintenancePaused || this.restartTimers.size > 0)
|
|
276
266
|
return null;
|
|
277
267
|
if ([...this.workers.values()].some((worker) => !worker.ready || worker.shuttingDown || this.isWorkerVisiblyBusy(worker)))
|
|
278
268
|
return null;
|
|
@@ -292,9 +282,20 @@ export class ThreadSupervisor {
|
|
|
292
282
|
}
|
|
293
283
|
this.maintenanceExecutionId = null;
|
|
294
284
|
this.maintenancePaused = false;
|
|
285
|
+
const pending = this.maintenanceMessages;
|
|
286
|
+
this.maintenanceMessages = [];
|
|
295
287
|
if (this.stopped)
|
|
296
|
-
return;
|
|
288
|
+
return; // Accepted ordinary messages are already durable.
|
|
289
|
+
// Executions queued behind the hold are admitted now, not on the next submit.
|
|
297
290
|
this.executionScheduler?.requestAdmission();
|
|
291
|
+
for (const { msg, threadId } of pending) {
|
|
292
|
+
try {
|
|
293
|
+
this.routeMessage(msg, threadId);
|
|
294
|
+
}
|
|
295
|
+
catch (err) {
|
|
296
|
+
logger.warn(`[supervisor] Could not resume queued thread=${threadId}: ${String(err)}`);
|
|
297
|
+
}
|
|
298
|
+
}
|
|
298
299
|
this.schedulePendingThreads();
|
|
299
300
|
};
|
|
300
301
|
}
|
|
@@ -305,16 +306,58 @@ export class ThreadSupervisor {
|
|
|
305
306
|
if (queue.hasOutstandingWork()) {
|
|
306
307
|
logger.system(`[supervisor] Recovered pending work for thread=${threadId}`);
|
|
307
308
|
}
|
|
309
|
+
else if (this.hasRunnableTasks(threadId)) {
|
|
310
|
+
logger.system(`[supervisor] Recovered runnable tasks for thread=${threadId}`);
|
|
311
|
+
}
|
|
308
312
|
}
|
|
309
313
|
this.schedulePendingThreads();
|
|
310
314
|
}
|
|
311
|
-
/**
|
|
315
|
+
/**
|
|
316
|
+
* Whether a thread without queued messages still has a task that may
|
|
317
|
+
* start (#1996 PR 5). Its worker queues the resume itself once it runs:
|
|
318
|
+
* the supervisor only starts it, it never writes the thread's task store.
|
|
319
|
+
* By design (owner 10-03): a task that is not due yet is not counted;
|
|
320
|
+
* its due or unblock wake comes from the existing calendar / event-trigger
|
|
321
|
+
* messages to the thread. Thread tasks never schedule wakes.
|
|
322
|
+
* TODO: a worker started this way whose first pass queues nothing (gated)
|
|
323
|
+
* has no idle timer until its first turn, so it holds a slot until evicted.
|
|
324
|
+
*/
|
|
325
|
+
hasRunnableTasks(threadId) {
|
|
326
|
+
if (!isThreadTasksEnabled(this.config))
|
|
327
|
+
return false;
|
|
328
|
+
return threadHasRunnableTasks(taskStorePathForThread(threadId), threadId);
|
|
329
|
+
}
|
|
330
|
+
/**
|
|
331
|
+
* Start persisted threads whenever a worker slot is available. Work remains
|
|
332
|
+
* in its queue.db when all slots are busy and is retried after a worker turn
|
|
333
|
+
* or exit, rather than being rejected or routed to the default thread.
|
|
334
|
+
*/
|
|
312
335
|
schedulePendingThreads() {
|
|
313
|
-
if (this.stopped || this.
|
|
336
|
+
if (this.stopped || this.maintenancePaused)
|
|
314
337
|
return;
|
|
315
|
-
for (const
|
|
316
|
-
if (
|
|
317
|
-
|
|
338
|
+
for (const threadId of this.threadQueues.keys()) {
|
|
339
|
+
if (this.workers.has(threadId))
|
|
340
|
+
continue;
|
|
341
|
+
if (this.restartTimers.has(threadId))
|
|
342
|
+
continue;
|
|
343
|
+
if (this.recoveryExhaustedThreads.has(threadId))
|
|
344
|
+
continue;
|
|
345
|
+
const queue = this.threadQueues.get(threadId);
|
|
346
|
+
if (!queue?.hasOutstandingWork() && !this.hasRunnableTasks(threadId))
|
|
347
|
+
continue;
|
|
348
|
+
if (this.getActiveWorkerCount() >= this.maxConcurrentThreads) {
|
|
349
|
+
const evicted = this.evictLeastRecentlyActiveIdleWorker(threadId);
|
|
350
|
+
if (!evicted && this.getActiveWorkerCount() >= this.maxConcurrentThreads)
|
|
351
|
+
break;
|
|
352
|
+
}
|
|
353
|
+
try {
|
|
354
|
+
this.spawnWorker(threadId);
|
|
355
|
+
}
|
|
356
|
+
catch (error) {
|
|
357
|
+
logger.warn(`[supervisor] Failed to recover thread=${threadId}: ${error instanceof Error ? error.message : String(error)}`);
|
|
358
|
+
this.recoveryExhaustedThreads.add(threadId);
|
|
359
|
+
this.notifyPendingSendersOfFailure(threadId, this.outstandingMessages(threadId, []));
|
|
360
|
+
}
|
|
318
361
|
}
|
|
319
362
|
}
|
|
320
363
|
/**
|
|
@@ -383,7 +426,7 @@ export class ThreadSupervisor {
|
|
|
383
426
|
}
|
|
384
427
|
// Thread-scoped engine switch: handled by the supervisor (write thread.json
|
|
385
428
|
// + restart worker), NOT forwarded to the worker.
|
|
386
|
-
const engine =
|
|
429
|
+
const engine = parseEngineCommand(msg.text);
|
|
387
430
|
if (engine !== null) {
|
|
388
431
|
this.switchThreadEngine(threadId, engine, msg);
|
|
389
432
|
return true;
|
|
@@ -409,32 +452,71 @@ export class ThreadSupervisor {
|
|
|
409
452
|
throw new Error("A previous run of this Lab is still stopping; retry this delivery");
|
|
410
453
|
return false;
|
|
411
454
|
}
|
|
455
|
+
let durableInserted = false;
|
|
412
456
|
if (!labRun) {
|
|
413
|
-
this.getThreadQueue(threadId, true).enqueue(msg);
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
457
|
+
durableInserted = this.getThreadQueue(threadId, true).enqueue(msg);
|
|
458
|
+
// A new provider/request id is an explicit retry after an exhausted
|
|
459
|
+
// startup/crash budget. Re-enable this thread without letting unrelated
|
|
460
|
+
// scheduler ticks revive it forever.
|
|
461
|
+
if (durableInserted && this.recoveryExhaustedThreads.delete(threadId)) {
|
|
462
|
+
this.crashCounts.delete(threadId);
|
|
418
463
|
}
|
|
464
|
+
}
|
|
465
|
+
if (this.maintenancePaused) {
|
|
466
|
+
this.maintenanceMessages.push({ msg, threadId });
|
|
467
|
+
if (isOwnerInitiatedMessage(msg))
|
|
468
|
+
this.onMaintenanceInput?.();
|
|
419
469
|
return true;
|
|
420
470
|
}
|
|
421
471
|
let state = this.workers.get(threadId);
|
|
422
472
|
if (state?.shuttingDown) {
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
473
|
+
logger.debug(`[supervisor] Worker shutting down, buffering message for thread=${threadId}`);
|
|
474
|
+
const wasVisibleBusy = this.isWorkerVisiblyBusy(state);
|
|
475
|
+
state.pendingMessages.push(msg);
|
|
476
|
+
this.emitBusyChangeIfNeeded(wasVisibleBusy, state);
|
|
477
|
+
return true;
|
|
478
|
+
}
|
|
479
|
+
if (!labRun && this.restartTimers.has(threadId)) {
|
|
480
|
+
logger.debug(`[supervisor] Worker restart already scheduled for thread=${threadId}`);
|
|
481
|
+
return true;
|
|
482
|
+
}
|
|
483
|
+
if (!labRun && this.recoveryExhaustedThreads.has(threadId) && !durableInserted) {
|
|
484
|
+
logger.debug(`[supervisor] Duplicate work remains dormant after recovery exhaustion: thread=${threadId}`);
|
|
485
|
+
return true;
|
|
426
486
|
}
|
|
427
487
|
if (!state) {
|
|
488
|
+
// Check concurrency limit before spawning. Idle workers are only a
|
|
489
|
+
// keep-warm optimization: they must not block a new logical thread from
|
|
490
|
+
// making progress. If the process pool is full, evict the least-recently
|
|
491
|
+
// active idle worker first, and reject only when every slot is busy (or
|
|
492
|
+
// still spawning/shutting down).
|
|
428
493
|
if (this.getActiveWorkerCount() >= this.maxConcurrentThreads) {
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
494
|
+
const evicted = this.evictLeastRecentlyActiveIdleWorker(threadId);
|
|
495
|
+
if (!evicted && this.getActiveWorkerCount() >= this.maxConcurrentThreads) {
|
|
496
|
+
if (labRun) {
|
|
497
|
+
// LabRun has its own Fin-app lease and is intentionally not stored
|
|
498
|
+
// in the ordinary thread queue. Preserve the pre-existing explicit
|
|
499
|
+
// failure contract instead of claiming work that was never saved.
|
|
500
|
+
logger.warn(`[supervisor] Concurrency limit reached (${this.maxConcurrentThreads}), rejecting LabRun thread=${threadId}`);
|
|
501
|
+
deliverReplyToMessage(this.channelManager, msg, `⚠️ Thread concurrency limit reached (${this.maxConcurrentThreads} active). Please try again later.`).catch(() => { });
|
|
502
|
+
return claimRejected;
|
|
503
|
+
}
|
|
504
|
+
logger.info(`[supervisor] Concurrency limit reached (${this.maxConcurrentThreads}); keeping thread=${threadId} queued`);
|
|
505
|
+
this.publishSyntheticActivity(threadId, "queued", "Waiting for a worker slot", [msg]);
|
|
506
|
+
// The message is already durably accepted in queue.db. Do not leak
|
|
507
|
+
// it into the default thread or ask the provider to retry.
|
|
508
|
+
return true;
|
|
509
|
+
}
|
|
432
510
|
}
|
|
433
511
|
state = this.spawnWorker(threadId, { bootstrapMessage: msg });
|
|
434
512
|
}
|
|
513
|
+
// Worker still spawning: buffer the message; flushed on `ready`.
|
|
435
514
|
if (!state.ready) {
|
|
515
|
+
logger.debug(`[supervisor] Worker spawning, buffering message for thread=${threadId}`);
|
|
436
516
|
const wasVisibleBusy = this.isWorkerVisiblyBusy(state);
|
|
437
517
|
state.pendingMessages.push(msg);
|
|
518
|
+
// Check all pending messages: only publish status if at least one is owner-initiated
|
|
519
|
+
this.publishSyntheticActivity(threadId, "queued", "Starting thread worker", state.pendingMessages);
|
|
438
520
|
this.emitBusyChangeIfNeeded(wasVisibleBusy, state);
|
|
439
521
|
return true;
|
|
440
522
|
}
|
|
@@ -442,11 +524,28 @@ export class ThreadSupervisor {
|
|
|
442
524
|
return true;
|
|
443
525
|
}
|
|
444
526
|
getActiveWorkerCount() {
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
527
|
+
let count = 0;
|
|
528
|
+
for (const state of this.workers.values()) {
|
|
529
|
+
if (!state.shuttingDown)
|
|
530
|
+
count += 1;
|
|
531
|
+
}
|
|
532
|
+
return count;
|
|
533
|
+
}
|
|
534
|
+
evictLeastRecentlyActiveIdleWorker(requestedThreadId) {
|
|
535
|
+
let candidate = null;
|
|
536
|
+
for (const state of this.workers.values()) {
|
|
537
|
+
// An internal executor between turns is not idle keep-warm capacity.
|
|
538
|
+
if (!state.ready || state.busy || state.shuttingDown || state.internalExecution)
|
|
539
|
+
continue;
|
|
540
|
+
if (!candidate || Date.parse(state.lastActivityAt) < Date.parse(candidate.lastActivityAt)) {
|
|
541
|
+
candidate = state;
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
if (!candidate)
|
|
545
|
+
return false;
|
|
546
|
+
logger.system(`[supervisor] Evicting idle worker thread=${candidate.threadId} to make room for thread=${requestedThreadId}`);
|
|
547
|
+
this.shutdownWorker(candidate.threadId, "idle-evicted");
|
|
548
|
+
return true;
|
|
450
549
|
}
|
|
451
550
|
/**
|
|
452
551
|
* Switch a thread's engine: persist the model/provider into thread.json, then
|
|
@@ -465,7 +564,6 @@ export class ThreadSupervisor {
|
|
|
465
564
|
return;
|
|
466
565
|
}
|
|
467
566
|
// Restart the running worker so it re-reads thread.json on next spawn.
|
|
468
|
-
void this.conversations?.stop(threadId);
|
|
469
567
|
const state = this.workers.get(threadId);
|
|
470
568
|
if (state) {
|
|
471
569
|
this.shutdownWorker(threadId, "engine-switch");
|
|
@@ -478,47 +576,6 @@ export class ThreadSupervisor {
|
|
|
478
576
|
startExecutions() {
|
|
479
577
|
this.executionScheduler?.start();
|
|
480
578
|
}
|
|
481
|
-
/** Foreground turns have their own admission pool; execution slots are unrelated. */
|
|
482
|
-
acquireConversationTurn(signal) {
|
|
483
|
-
return this.conversations?.acquireTurn(signal) ?? Promise.resolve(() => undefined);
|
|
484
|
-
}
|
|
485
|
-
handleConversationEvent(conversationId, event, reply) {
|
|
486
|
-
if (event.type === "reply") {
|
|
487
|
-
if (!isDeliveryTarget(event.target)
|
|
488
|
-
|| (event.ownerEscalation === true && !this.isOwnerEscalationTarget(event.target))) {
|
|
489
|
-
if (event.replyId)
|
|
490
|
-
reply({ type: "reply-result", replyId: event.replyId, ok: false, errorMessage: "Invalid conversation reply target" });
|
|
491
|
-
return;
|
|
492
|
-
}
|
|
493
|
-
void this.channelManager.deliver(event.target, event.message, event.attachments, {
|
|
494
|
-
businessThreadId: event.ownerEscalation ? DEFAULT_THREAD_ID : conversationId,
|
|
495
|
-
...(event.kind ? { kind: event.kind } : {}),
|
|
496
|
-
}).then(() => {
|
|
497
|
-
this.replyEscalation.success(conversationId);
|
|
498
|
-
if (event.replyId)
|
|
499
|
-
reply({ type: "reply-result", replyId: event.replyId, ok: true });
|
|
500
|
-
}).catch((error) => {
|
|
501
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
502
|
-
if (event.replyId)
|
|
503
|
-
reply({ type: "reply-result", replyId: event.replyId, ok: false, errorMessage: message });
|
|
504
|
-
if (this.replyEscalation.failure(conversationId))
|
|
505
|
-
void this.alertOwnerOfThreadDeliveryFailure(conversationId, event.target, message);
|
|
506
|
-
});
|
|
507
|
-
}
|
|
508
|
-
else if (event.type === "typing") {
|
|
509
|
-
if (isDeliveryTarget(event.target))
|
|
510
|
-
void this.channelManager.sendTyping(event.target).catch(() => { });
|
|
511
|
-
}
|
|
512
|
-
else if (event.type === "status" && event.activity) {
|
|
513
|
-
this.latestActivities.set(conversationId, event.activity);
|
|
514
|
-
this.channelManager.publishThreadActivity(conversationId, event.activity, this.agentInstanceId);
|
|
515
|
-
}
|
|
516
|
-
else if (event.type === "feedback") {
|
|
517
|
-
const persisted = this.feedbackOutbox.enqueue(event.feedback) ?? this.feedbackOutbox.getPersisted(event.feedback.id);
|
|
518
|
-
if (persisted)
|
|
519
|
-
this.onFeedback?.(persisted);
|
|
520
|
-
}
|
|
521
|
-
}
|
|
522
579
|
/**
|
|
523
580
|
* The execution API for trusted in-process callers. Submissions are
|
|
524
581
|
* accepted only after the command, record and snapshot are durable.
|
|
@@ -528,14 +585,12 @@ export class ThreadSupervisor {
|
|
|
528
585
|
if (!store)
|
|
529
586
|
return null;
|
|
530
587
|
return {
|
|
531
|
-
atomic: (operation) => store.atomic(operation),
|
|
532
|
-
attemptsForTask: (taskId, scope) => store.attemptsForTask(taskId, scope),
|
|
533
588
|
submit: (envelope, input) => {
|
|
534
589
|
const executionId = `exec-${randomUUID()}`;
|
|
535
590
|
const outcome = store.submit(envelope, {
|
|
536
591
|
...input,
|
|
537
592
|
executionId,
|
|
538
|
-
snapshot: this.signExecutionSnapshotFor(executionId
|
|
593
|
+
snapshot: this.signExecutionSnapshotFor(executionId),
|
|
539
594
|
});
|
|
540
595
|
if (outcome.kind === "ok" && !outcome.replay) {
|
|
541
596
|
if (this.maintenancePaused)
|
|
@@ -622,11 +677,21 @@ export class ThreadSupervisor {
|
|
|
622
677
|
: !this.maintenancePaused;
|
|
623
678
|
if (!admitted)
|
|
624
679
|
return 0;
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
680
|
+
// Foreman tasks share the overall pool but never take more than their cap.
|
|
681
|
+
// A stopping executor still counts: its slot frees only once it exits.
|
|
682
|
+
if (record.caller.kind === "general") {
|
|
683
|
+
let foremanWorkers = 0;
|
|
684
|
+
for (const state of this.workers.values())
|
|
685
|
+
if (state.foreman)
|
|
686
|
+
foremanWorkers += 1;
|
|
687
|
+
if (foremanWorkers >= MAX_PARALLEL_FOREMAN_TASKS)
|
|
688
|
+
return 0;
|
|
689
|
+
}
|
|
690
|
+
const free = this.maxConcurrentThreads - this.getActiveWorkerCount();
|
|
691
|
+
// An idle keep-warm worker yields its slot rather than holding admission
|
|
692
|
+
// until its idle timeout.
|
|
693
|
+
if (free > 0 || !this.evictLeastRecentlyActiveIdleWorker(record.executionId))
|
|
694
|
+
return free;
|
|
630
695
|
return this.maxConcurrentThreads - this.getActiveWorkerCount();
|
|
631
696
|
}
|
|
632
697
|
/**
|
|
@@ -835,9 +900,6 @@ export class ThreadSupervisor {
|
|
|
835
900
|
* Spawn a worker process for the given threadId.
|
|
836
901
|
*/
|
|
837
902
|
spawnWorker(threadId, options = {}) {
|
|
838
|
-
if (!options.execution && !isLabRunThreadId(threadId)) {
|
|
839
|
-
throw new Error("Only admitted Tasks and LabRuns may start worker processes");
|
|
840
|
-
}
|
|
841
903
|
logger.system(`[supervisor] Spawning worker for thread=${threadId}`);
|
|
842
904
|
const threadMeta = ensureThreadApplication(threadId, options.bootstrapMessage?.app);
|
|
843
905
|
threadApplication(threadId, this.config);
|
|
@@ -898,6 +960,7 @@ export class ThreadSupervisor {
|
|
|
898
960
|
busy: false,
|
|
899
961
|
taskDescription: "spawning",
|
|
900
962
|
lastActivityAt: new Date().toISOString(),
|
|
963
|
+
idleTimer: null,
|
|
901
964
|
shuttingDown: false,
|
|
902
965
|
pendingMessages: [],
|
|
903
966
|
internalExecution,
|
|
@@ -946,13 +1009,16 @@ export class ThreadSupervisor {
|
|
|
946
1009
|
this.handleReply(state, data);
|
|
947
1010
|
break;
|
|
948
1011
|
case "typing":
|
|
1012
|
+
this.handleTyping(state, data);
|
|
949
1013
|
break;
|
|
950
1014
|
case "status":
|
|
951
1015
|
this.handleStatus(state, data);
|
|
952
1016
|
break;
|
|
953
1017
|
case "feedback":
|
|
1018
|
+
this.handleFeedback(state, data);
|
|
954
1019
|
break;
|
|
955
1020
|
case "turn-complete":
|
|
1021
|
+
this.handleTurnComplete(state, data);
|
|
956
1022
|
break;
|
|
957
1023
|
case "execution-bootstrap-ready":
|
|
958
1024
|
this.handleExecutionBootstrap(state);
|
|
@@ -992,25 +1058,15 @@ export class ThreadSupervisor {
|
|
|
992
1058
|
};
|
|
993
1059
|
if (typeof msg.requestId !== "string")
|
|
994
1060
|
return;
|
|
995
|
-
if (state.internalExecution
|
|
996
|
-
|
|
997
|
-
const record = expected ? this.executionStore?.get(expected.executionId) : null;
|
|
998
|
-
if (!record || state.shuttingDown || record.state !== "running" || record.caller.kind !== "general"
|
|
999
|
-
|| record.workerInstanceId !== expected?.generation || !record.payload.businessRead || !this.handleForemanBusinessRead) {
|
|
1000
|
-
answer({ ok: false, text: "Business reads require a live authorized Foreman execution" });
|
|
1001
|
-
return;
|
|
1002
|
-
}
|
|
1003
|
-
const workspace = record.payload.workspaceRef ?? path.join(getThreadDir(state.threadId), "workspace");
|
|
1004
|
-
this.handleForemanBusinessRead(record.executionId, msg.command, workspace).then((result) => {
|
|
1005
|
-
const current = this.executionStore?.get(record.executionId);
|
|
1006
|
-
if (current?.state === "running" && current.workerInstanceId === expected.generation)
|
|
1007
|
-
answer(result);
|
|
1008
|
-
else
|
|
1009
|
-
answer({ ok: false, text: "The execution stopped before the business read completed" });
|
|
1010
|
-
}).catch((error) => answer({ ok: false, text: `Business read failed: ${error instanceof Error ? error.message : String(error)}` }));
|
|
1061
|
+
if (state.internalExecution || !this.handleForemanCommand) {
|
|
1062
|
+
answer({ ok: false, text: "Error: background tasks are not available in this conversation." });
|
|
1011
1063
|
return;
|
|
1012
1064
|
}
|
|
1013
|
-
|
|
1065
|
+
this.handleForemanCommand(state.threadId, msg.command)
|
|
1066
|
+
.then(answer)
|
|
1067
|
+
.catch((error) => {
|
|
1068
|
+
answer({ ok: false, text: `Error: ${error instanceof Error ? error.message : String(error)}` });
|
|
1069
|
+
});
|
|
1014
1070
|
}
|
|
1015
1071
|
/**
|
|
1016
1072
|
* The executor is listening: hand it the admitted record from the ledger.
|
|
@@ -1033,15 +1089,22 @@ export class ThreadSupervisor {
|
|
|
1033
1089
|
type: "execution-start",
|
|
1034
1090
|
record,
|
|
1035
1091
|
unfinishedTools: store.unfinishedToolCalls(record.executionId),
|
|
1036
|
-
|
|
1037
|
-
// receipt. A crash between that cursor and the SDK's transcript write
|
|
1038
|
-
// must not erase an accepted answer. Replay the authoritative history.
|
|
1039
|
-
pendingInputs: store.pendingInputs(record.executionId, 0),
|
|
1092
|
+
pendingInputs: store.pendingInputs(record.executionId, record.processedInputSeq),
|
|
1040
1093
|
}, (error) => {
|
|
1041
1094
|
if (error)
|
|
1042
1095
|
logger.debug(`[supervisor] Execution start IPC unavailable for ${state.threadId}: ${error.message}`);
|
|
1043
1096
|
});
|
|
1044
1097
|
}
|
|
1098
|
+
handleFeedback(state, msg) {
|
|
1099
|
+
logger.debug(`[supervisor] Feedback received from thread=${state.threadId}: ${msg.feedback.id}`);
|
|
1100
|
+
const persisted = this.feedbackOutbox.enqueue(msg.feedback)
|
|
1101
|
+
?? this.feedbackOutbox.getPersisted(msg.feedback.id);
|
|
1102
|
+
if (!persisted)
|
|
1103
|
+
return;
|
|
1104
|
+
this.onFeedback?.(persisted);
|
|
1105
|
+
// Main HeartbeatManager will discover this durable item on its next tick.
|
|
1106
|
+
logger.debug(`[supervisor] Persisted feedback from thread=${state.threadId}: ${persisted.id}`);
|
|
1107
|
+
}
|
|
1045
1108
|
handleLabRuntimeBootstrapReady(state) {
|
|
1046
1109
|
if (!state.labRuntimePolicy || !isLabRunThreadId(state.threadId)) {
|
|
1047
1110
|
logger.warn(`[supervisor] Refusing Lab credential bootstrap for non-Lab thread=${state.threadId}`);
|
|
@@ -1119,7 +1182,47 @@ export class ThreadSupervisor {
|
|
|
1119
1182
|
}
|
|
1120
1183
|
}
|
|
1121
1184
|
handleReply(state, msg) {
|
|
1122
|
-
|
|
1185
|
+
if (state.internalExecution) {
|
|
1186
|
+
this.sendReplyResult(state, msg.replyId, false, "Internal worker executions cannot deliver external messages");
|
|
1187
|
+
return;
|
|
1188
|
+
}
|
|
1189
|
+
if (!isDeliveryTarget(msg.target)) {
|
|
1190
|
+
const errorMessage = "worker reply contained an invalid target";
|
|
1191
|
+
logger.warn(`[supervisor] ${errorMessage}`);
|
|
1192
|
+
this.sendReplyResult(state, msg.replyId, false, errorMessage);
|
|
1193
|
+
return;
|
|
1194
|
+
}
|
|
1195
|
+
// A worker's sends belong to its thread, so mail adapters record provider
|
|
1196
|
+
// replies against it. An owner escalation must not: the owner's answer is
|
|
1197
|
+
// private to the owner's own (default) conversation. The marker is only
|
|
1198
|
+
// honored for a real owner target resolved here, in the main process.
|
|
1199
|
+
let businessThreadId = state.threadId;
|
|
1200
|
+
if (msg.ownerEscalation === true) {
|
|
1201
|
+
if (!this.isOwnerEscalationTarget(msg.target)) {
|
|
1202
|
+
const errorMessage = "owner escalation target is not a bound owner target";
|
|
1203
|
+
logger.warn(`[supervisor] Refused reply from thread=${state.threadId}: ${errorMessage}`);
|
|
1204
|
+
this.sendReplyResult(state, msg.replyId, false, errorMessage);
|
|
1205
|
+
return;
|
|
1206
|
+
}
|
|
1207
|
+
businessThreadId = DEFAULT_THREAD_ID;
|
|
1208
|
+
}
|
|
1209
|
+
logger.debug(`[supervisor] Relaying reply from channel=${msg.target.channel}`);
|
|
1210
|
+
this.channelManager.deliver(msg.target, msg.message, msg.attachments, {
|
|
1211
|
+
businessThreadId,
|
|
1212
|
+
...(msg.kind ? { kind: msg.kind } : {}),
|
|
1213
|
+
})
|
|
1214
|
+
.then(() => {
|
|
1215
|
+
this.replyEscalation.success(state.threadId);
|
|
1216
|
+
this.sendReplyResult(state, msg.replyId, true);
|
|
1217
|
+
})
|
|
1218
|
+
.catch((err) => {
|
|
1219
|
+
const errorMessage = err instanceof Error ? err.message : String(err);
|
|
1220
|
+
logger.warn(`[supervisor] Failed to relay reply: ${errorMessage}`);
|
|
1221
|
+
this.sendReplyResult(state, msg.replyId, false, errorMessage);
|
|
1222
|
+
if (this.replyEscalation.failure(state.threadId)) {
|
|
1223
|
+
void this.alertOwnerOfThreadDeliveryFailure(state.threadId, msg.target, errorMessage);
|
|
1224
|
+
}
|
|
1225
|
+
});
|
|
1123
1226
|
}
|
|
1124
1227
|
/** True when `target` is one of the owner's usable bound targets (no reply context). */
|
|
1125
1228
|
isOwnerEscalationTarget(target) {
|
|
@@ -1175,6 +1278,17 @@ export class ThreadSupervisor {
|
|
|
1175
1278
|
logger.debug(`[supervisor] Could not ack reply ${replyId}: ${err instanceof Error ? err.message : String(err)}`);
|
|
1176
1279
|
}
|
|
1177
1280
|
}
|
|
1281
|
+
handleTyping(state, msg) {
|
|
1282
|
+
if (state.internalExecution)
|
|
1283
|
+
return;
|
|
1284
|
+
if (!isDeliveryTarget(msg.target)) {
|
|
1285
|
+
logger.warn("[supervisor] Worker typing contained an invalid target");
|
|
1286
|
+
return;
|
|
1287
|
+
}
|
|
1288
|
+
this.channelManager.sendTyping(msg.target).catch(() => {
|
|
1289
|
+
// best-effort
|
|
1290
|
+
});
|
|
1291
|
+
}
|
|
1178
1292
|
handleStatus(state, msg) {
|
|
1179
1293
|
if (msg.activity && !state.privateActivity) {
|
|
1180
1294
|
const current = state.activity;
|
|
@@ -1205,6 +1319,34 @@ export class ThreadSupervisor {
|
|
|
1205
1319
|
this.executionScheduler?.requestAdmission();
|
|
1206
1320
|
}
|
|
1207
1321
|
}
|
|
1322
|
+
handleTurnComplete(state, _msg) {
|
|
1323
|
+
logger.system(`[supervisor] Turn complete from thread=${state.threadId}`);
|
|
1324
|
+
// A completed turn proves the worker is healthy — reset its crash budget so
|
|
1325
|
+
// sporadic crashes spread over a long lifetime don't exhaust the cap.
|
|
1326
|
+
this.crashCounts.delete(state.threadId);
|
|
1327
|
+
this.recoveryExhaustedThreads.delete(state.threadId);
|
|
1328
|
+
// Billing is NOT handled here: the worker's own WakeCycleRunner already
|
|
1329
|
+
// wrote the wake-cycle row into the shared transcript-memory.db (WAL-safe).
|
|
1330
|
+
// Memory dreaming is not triggered here either: it rewrites and deletes
|
|
1331
|
+
// files in the shared memories/ tree, so it runs on the deep-night sweep
|
|
1332
|
+
// only (owned by NightlyMaintenance), not once per wake cycle.
|
|
1333
|
+
this.armIdleTimer(state);
|
|
1334
|
+
this.schedulePendingThreads();
|
|
1335
|
+
this.executionScheduler?.requestAdmission();
|
|
1336
|
+
}
|
|
1337
|
+
armIdleTimer(state) {
|
|
1338
|
+
if (state.idleTimer !== null) {
|
|
1339
|
+
clearTimeout(state.idleTimer);
|
|
1340
|
+
}
|
|
1341
|
+
state.idleTimer = setTimeout(() => {
|
|
1342
|
+
if (state.busy) {
|
|
1343
|
+
logger.debug(`[supervisor] Worker still busy at idle timeout, extending: thread=${state.threadId}`);
|
|
1344
|
+
this.armIdleTimer(state);
|
|
1345
|
+
return;
|
|
1346
|
+
}
|
|
1347
|
+
this.shutdownWorker(state.threadId, "idle-timeout");
|
|
1348
|
+
}, this.idleTimeoutMs);
|
|
1349
|
+
}
|
|
1208
1350
|
handleWorkerExit(threadId, code, signal) {
|
|
1209
1351
|
const state = this.workers.get(threadId);
|
|
1210
1352
|
if (!state)
|
|
@@ -1212,6 +1354,11 @@ export class ThreadSupervisor {
|
|
|
1212
1354
|
const wasReady = state.ready;
|
|
1213
1355
|
logger.system(`[supervisor] Worker exited: thread=${threadId} code=${code} signal=${signal} wasReady=${wasReady}`);
|
|
1214
1356
|
// Clear idle timer.
|
|
1357
|
+
if (state.idleTimer !== null) {
|
|
1358
|
+
clearTimeout(state.idleTimer);
|
|
1359
|
+
state.idleTimer = null;
|
|
1360
|
+
}
|
|
1361
|
+
const pendingMessages = state.pendingMessages;
|
|
1215
1362
|
if (state.busy && state.activity && !state.shuttingDown) {
|
|
1216
1363
|
const now = new Date().toISOString();
|
|
1217
1364
|
const failedActivity = {
|
|
@@ -1242,8 +1389,68 @@ export class ThreadSupervisor {
|
|
|
1242
1389
|
this.schedulePendingThreads();
|
|
1243
1390
|
return;
|
|
1244
1391
|
}
|
|
1245
|
-
//
|
|
1246
|
-
//
|
|
1392
|
+
// LabRun workers are single-shot executors. A retry is a new durable run
|
|
1393
|
+
// with a new `fin-lab-run-<runId>` thread; never revive the old session.
|
|
1394
|
+
if (isLabRunThreadId(threadId)) {
|
|
1395
|
+
this.crashCounts.delete(threadId);
|
|
1396
|
+
this.schedulePendingThreads();
|
|
1397
|
+
return;
|
|
1398
|
+
}
|
|
1399
|
+
// Clean exit, or any exit during a requested shutdown (a graceful stop that
|
|
1400
|
+
// ends non-zero is not a crash): don't count towards the crash cap. Respawn
|
|
1401
|
+
// only when messages arrived during the shutdown window — but never after
|
|
1402
|
+
// the supervisor itself has been stopped.
|
|
1403
|
+
if (code === 0 || state.shuttingDown) {
|
|
1404
|
+
if (code === 0)
|
|
1405
|
+
this.crashCounts.delete(threadId);
|
|
1406
|
+
if (pendingMessages.length > 0 && !this.stopped) {
|
|
1407
|
+
this.spawnWorkerForPendingMessages(threadId, pendingMessages, "shutdown-respawn", state.labRuntimePolicy, state.executionSnapshot);
|
|
1408
|
+
}
|
|
1409
|
+
this.schedulePendingThreads();
|
|
1410
|
+
return;
|
|
1411
|
+
}
|
|
1412
|
+
// Crash recovery: restart if crashed unexpectedly and under threshold.
|
|
1413
|
+
const MAX_CRASH_COUNT = 3;
|
|
1414
|
+
const nextCrashCount = wasReady ? (this.crashCounts.get(threadId) ?? 0) + 1 : this.crashCounts.get(threadId) ?? 0;
|
|
1415
|
+
if (wasReady) {
|
|
1416
|
+
this.crashCounts.set(threadId, nextCrashCount);
|
|
1417
|
+
}
|
|
1418
|
+
if (wasReady && nextCrashCount <= MAX_CRASH_COUNT && !this.stopped) {
|
|
1419
|
+
logger.warn(`[supervisor] Worker crashed, restarting (attempt ${nextCrashCount}/${MAX_CRASH_COUNT}): thread=${threadId}`);
|
|
1420
|
+
// Re-spawn after a brief delay.
|
|
1421
|
+
const trustedLabRuntimePolicy = state.labRuntimePolicy;
|
|
1422
|
+
const trustedExecutionSnapshot = state.executionSnapshot;
|
|
1423
|
+
const timer = setTimeout(() => {
|
|
1424
|
+
this.restartTimers.delete(threadId);
|
|
1425
|
+
if (this.stopped || this.maintenancePaused || this.workers.has(threadId))
|
|
1426
|
+
return;
|
|
1427
|
+
try {
|
|
1428
|
+
const next = this.spawnWorker(threadId, {
|
|
1429
|
+
bootstrapMessage: pendingMessages[0],
|
|
1430
|
+
trustedLabRuntimePolicy,
|
|
1431
|
+
trustedExecutionSnapshot,
|
|
1432
|
+
});
|
|
1433
|
+
this.enqueuePendingWork(next, pendingMessages);
|
|
1434
|
+
}
|
|
1435
|
+
catch (error) {
|
|
1436
|
+
this.handleWorkerRespawnFailure(threadId, pendingMessages, "crash-restart", error);
|
|
1437
|
+
}
|
|
1438
|
+
}, this.crashRestartDelayMs);
|
|
1439
|
+
this.restartTimers.set(threadId, timer);
|
|
1440
|
+
this.schedulePendingThreads();
|
|
1441
|
+
return;
|
|
1442
|
+
}
|
|
1443
|
+
// Not restarting: either the worker died before ever becoming ready
|
|
1444
|
+
// (startup failure — restarting would loop) or the crash cap is exhausted.
|
|
1445
|
+
// Tell the senders of any buffered messages instead of dropping silently.
|
|
1446
|
+
if (nextCrashCount > MAX_CRASH_COUNT) {
|
|
1447
|
+
logger.err(`[supervisor] Worker crash limit exceeded, not restarting: thread=${threadId}`);
|
|
1448
|
+
}
|
|
1449
|
+
else {
|
|
1450
|
+
logger.err(`[supervisor] Worker failed during startup, not restarting: thread=${threadId}`);
|
|
1451
|
+
}
|
|
1452
|
+
this.recoveryExhaustedThreads.add(threadId);
|
|
1453
|
+
this.notifyPendingSendersOfFailure(threadId, this.outstandingMessages(threadId, pendingMessages));
|
|
1247
1454
|
this.schedulePendingThreads();
|
|
1248
1455
|
}
|
|
1249
1456
|
/**
|
|
@@ -1266,6 +1473,7 @@ export class ThreadSupervisor {
|
|
|
1266
1473
|
this.executionScheduler?.handleWorkerExit(state.execution.executionId, code, signal, { hostStopping, groupStopped });
|
|
1267
1474
|
}
|
|
1268
1475
|
else if (state.labExecution) {
|
|
1476
|
+
this.crashCounts.delete(state.threadId);
|
|
1269
1477
|
if (groupStopped) {
|
|
1270
1478
|
this.confirmLabExecutionStopped(state.labExecution, code);
|
|
1271
1479
|
}
|
|
@@ -1279,12 +1487,96 @@ export class ThreadSupervisor {
|
|
|
1279
1487
|
.finally(() => { this.pendingGroupReaps.delete(reap); });
|
|
1280
1488
|
this.pendingGroupReaps.add(reap);
|
|
1281
1489
|
}
|
|
1490
|
+
spawnWorkerForPendingMessages(threadId, pending, reason, trustedLabRuntimePolicy, trustedExecutionSnapshot) {
|
|
1491
|
+
if (pending.length === 0)
|
|
1492
|
+
return;
|
|
1493
|
+
if (this.getActiveWorkerCount() >= this.maxConcurrentThreads) {
|
|
1494
|
+
const evicted = this.evictLeastRecentlyActiveIdleWorker(threadId);
|
|
1495
|
+
if (!evicted && this.getActiveWorkerCount() >= this.maxConcurrentThreads) {
|
|
1496
|
+
logger.warn(`[supervisor] Cannot respawn worker for thread=${threadId} after ${reason}: concurrency limit reached`);
|
|
1497
|
+
if (isLabRunThreadId(threadId)) {
|
|
1498
|
+
this.notifyPendingSendersOfFailure(threadId, pending);
|
|
1499
|
+
}
|
|
1500
|
+
this.schedulePendingThreads();
|
|
1501
|
+
return;
|
|
1502
|
+
}
|
|
1503
|
+
}
|
|
1504
|
+
try {
|
|
1505
|
+
const next = this.spawnWorker(threadId, {
|
|
1506
|
+
bootstrapMessage: pending[0],
|
|
1507
|
+
trustedLabRuntimePolicy,
|
|
1508
|
+
trustedExecutionSnapshot,
|
|
1509
|
+
});
|
|
1510
|
+
this.enqueuePendingWork(next, pending);
|
|
1511
|
+
}
|
|
1512
|
+
catch (error) {
|
|
1513
|
+
this.handleWorkerRespawnFailure(threadId, pending, reason, error);
|
|
1514
|
+
}
|
|
1515
|
+
}
|
|
1516
|
+
handleWorkerRespawnFailure(threadId, pending, reason, error) {
|
|
1517
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1518
|
+
logger.err(`[supervisor] Failed to respawn worker thread=${threadId} after ${reason}: ${message}`);
|
|
1519
|
+
this.publishSyntheticActivity(threadId, "failed", "Worker restart failed");
|
|
1520
|
+
this.recoveryExhaustedThreads.add(threadId);
|
|
1521
|
+
this.notifyPendingSendersOfFailure(threadId, this.outstandingMessages(threadId, pending));
|
|
1522
|
+
}
|
|
1523
|
+
outstandingMessages(threadId, pending) {
|
|
1524
|
+
const combined = [
|
|
1525
|
+
...pending,
|
|
1526
|
+
...(this.threadQueues.get(threadId)?.outstandingSnapshot() ?? []),
|
|
1527
|
+
];
|
|
1528
|
+
const seen = new Set();
|
|
1529
|
+
return combined.filter((message) => {
|
|
1530
|
+
const key = `${message.channel}:${message.sender}:${message.id}`;
|
|
1531
|
+
if (seen.has(key))
|
|
1532
|
+
return false;
|
|
1533
|
+
seen.add(key);
|
|
1534
|
+
return true;
|
|
1535
|
+
});
|
|
1536
|
+
}
|
|
1282
1537
|
freezeLabRuntimePolicy(envelope) {
|
|
1283
1538
|
return Object.freeze({
|
|
1284
1539
|
payload: envelope.payload,
|
|
1285
1540
|
signature: envelope.signature,
|
|
1286
1541
|
});
|
|
1287
1542
|
}
|
|
1543
|
+
publishSyntheticActivity(threadId, state, label, messages) {
|
|
1544
|
+
if (!threadId.startsWith("fin-") || threadId.startsWith("fin-lab-"))
|
|
1545
|
+
return;
|
|
1546
|
+
// Suppress owner-facing status for passive-only wakes (email/external-wake).
|
|
1547
|
+
// Only publish when at least one message is owner-initiated (Telegram/Discord/etc.).
|
|
1548
|
+
if (messages && messages.length > 0 && !messages.some(isOwnerInitiatedMessage)) {
|
|
1549
|
+
logger.debug(`[supervisor] Suppressing synthetic activity for thread=${threadId}: all messages are passive ingress`);
|
|
1550
|
+
return;
|
|
1551
|
+
}
|
|
1552
|
+
const now = new Date().toISOString();
|
|
1553
|
+
const previous = this.latestActivities.get(threadId);
|
|
1554
|
+
const activity = {
|
|
1555
|
+
runId: randomUUID(),
|
|
1556
|
+
sequence: 1,
|
|
1557
|
+
state,
|
|
1558
|
+
currentStep: label,
|
|
1559
|
+
todos: [],
|
|
1560
|
+
updatedAt: now,
|
|
1561
|
+
...(state === "queued" ? { heartbeatAt: now } : { completedAt: now }),
|
|
1562
|
+
activities: [{ state, label, at: now }],
|
|
1563
|
+
workerInstanceId: previous?.workerInstanceId ?? randomUUID(),
|
|
1564
|
+
};
|
|
1565
|
+
this.latestActivities.set(threadId, activity);
|
|
1566
|
+
this.channelManager.publishThreadActivity(threadId, activity, this.agentInstanceId);
|
|
1567
|
+
}
|
|
1568
|
+
/** Best-effort failure notice to the sender(s) of messages we could not process. */
|
|
1569
|
+
notifyPendingSendersOfFailure(threadId, pending) {
|
|
1570
|
+
const notified = new Set();
|
|
1571
|
+
for (const m of pending) {
|
|
1572
|
+
const key = `${m.channel}:${m.sender}`;
|
|
1573
|
+
if (notified.has(key))
|
|
1574
|
+
continue;
|
|
1575
|
+
notified.add(key);
|
|
1576
|
+
deliverReplyToMessage(this.channelManager, m, `⚠️ Thread "${threadId}" failed to start and your message could not be processed. Please try again later or contact the owner.`)
|
|
1577
|
+
.catch(() => { });
|
|
1578
|
+
}
|
|
1579
|
+
}
|
|
1288
1580
|
/**
|
|
1289
1581
|
* Deliver a message to the worker via IPC.
|
|
1290
1582
|
*/
|
|
@@ -1330,6 +1622,7 @@ export class ThreadSupervisor {
|
|
|
1330
1622
|
...(snapshot.length > 0 ? { activeThreadsSnapshot: snapshot } : {}),
|
|
1331
1623
|
};
|
|
1332
1624
|
state.process.send(ipcMsg);
|
|
1625
|
+
this.armIdleTimer(state);
|
|
1333
1626
|
}
|
|
1334
1627
|
authority() {
|
|
1335
1628
|
return this.executionAuthority;
|
|
@@ -1338,15 +1631,11 @@ export class ThreadSupervisor {
|
|
|
1338
1631
|
return this.authority().labPolicy.publicKey;
|
|
1339
1632
|
}
|
|
1340
1633
|
/** Freeze and sign a new generic execution's snapshot with this host's authority. */
|
|
1341
|
-
signExecutionSnapshotFor(executionId, budget = DEFAULT_LAB_RUN_BUDGET
|
|
1342
|
-
const meta = caller?.kind === "general" ? readThreadMeta(caller.ref) : null;
|
|
1343
|
-
const selection = meta?.engineOverride
|
|
1344
|
-
? { engineOverride: meta.engineOverride, pinnedModel: meta.pinnedModel }
|
|
1345
|
-
: { engineOverride: this.config.engineOverride, pinnedModel: this.config.pinnedModel };
|
|
1634
|
+
signExecutionSnapshotFor(executionId, budget = DEFAULT_LAB_RUN_BUDGET) {
|
|
1346
1635
|
return JSON.stringify(signExecutionSnapshot({
|
|
1347
1636
|
version: EXECUTION_SNAPSHOT_VERSION,
|
|
1348
1637
|
executionId,
|
|
1349
|
-
model: freezeExecutionModel(
|
|
1638
|
+
model: freezeExecutionModel({ engineOverride: this.config.engineOverride, pinnedModel: this.config.pinnedModel }, getLlmRouting(), this.config.provider),
|
|
1350
1639
|
budget,
|
|
1351
1640
|
}, this.authority().snapshot));
|
|
1352
1641
|
}
|
|
@@ -1400,6 +1689,10 @@ export class ThreadSupervisor {
|
|
|
1400
1689
|
return;
|
|
1401
1690
|
state.shuttingDown = true;
|
|
1402
1691
|
logger.system(`[supervisor] Shutting down worker: thread=${threadId} reason=${reason}`);
|
|
1692
|
+
if (state.idleTimer !== null) {
|
|
1693
|
+
clearTimeout(state.idleTimer);
|
|
1694
|
+
state.idleTimer = null;
|
|
1695
|
+
}
|
|
1403
1696
|
const shutdownMsg = {
|
|
1404
1697
|
type: "shutdown",
|
|
1405
1698
|
reason,
|
|
@@ -1434,7 +1727,9 @@ export class ThreadSupervisor {
|
|
|
1434
1727
|
logger.system(`[supervisor] Shutting down all workers (${this.workers.size} active)`);
|
|
1435
1728
|
this.stopped = true;
|
|
1436
1729
|
this.executionScheduler?.stop();
|
|
1437
|
-
const
|
|
1730
|
+
for (const timer of this.restartTimers.values())
|
|
1731
|
+
clearTimeout(timer);
|
|
1732
|
+
this.restartTimers.clear();
|
|
1438
1733
|
// Initiate graceful shutdown for all workers
|
|
1439
1734
|
for (const [threadId] of this.workers) {
|
|
1440
1735
|
this.shutdownWorker(threadId, "supervisor-shutdown");
|
|
@@ -1456,7 +1751,7 @@ export class ThreadSupervisor {
|
|
|
1456
1751
|
});
|
|
1457
1752
|
exitPromises.push(exitPromise);
|
|
1458
1753
|
}
|
|
1459
|
-
await Promise.all(
|
|
1754
|
+
await Promise.all(exitPromises);
|
|
1460
1755
|
// Executor groups are confirmed empty (or left for the next host) before the ledger closes.
|
|
1461
1756
|
await Promise.all([...this.pendingGroupReaps]);
|
|
1462
1757
|
for (const queue of this.threadQueues.values()) {
|
|
@@ -1475,18 +1770,23 @@ export class ThreadSupervisor {
|
|
|
1475
1770
|
this.onBusyChange?.();
|
|
1476
1771
|
}
|
|
1477
1772
|
}
|
|
1773
|
+
enqueuePendingWork(state, pending) {
|
|
1774
|
+
if (pending.length === 0)
|
|
1775
|
+
return;
|
|
1776
|
+
const wasVisibleBusy = this.isWorkerVisiblyBusy(state);
|
|
1777
|
+
state.pendingMessages.push(...pending);
|
|
1778
|
+
this.emitBusyChangeIfNeeded(wasVisibleBusy, state);
|
|
1779
|
+
}
|
|
1478
1780
|
/**
|
|
1479
1781
|
* Get a snapshot of all active (ready) worker threads for cross-thread
|
|
1480
1782
|
* awareness. Does not include the main "default" thread.
|
|
1481
1783
|
*/
|
|
1482
1784
|
getActiveThreadsSnapshot() {
|
|
1483
|
-
const result =
|
|
1785
|
+
const result = [];
|
|
1484
1786
|
for (const [threadId, state] of this.workers) {
|
|
1485
1787
|
if (state.ready && !state.shuttingDown) {
|
|
1486
1788
|
result.push({
|
|
1487
1789
|
threadId,
|
|
1488
|
-
kind: state.internalExecution ? "execution" : "conversation",
|
|
1489
|
-
...(state.execution ? { taskId: this.executionStore?.get(state.execution.executionId)?.taskId } : {}),
|
|
1490
1790
|
busy: state.busy,
|
|
1491
1791
|
taskDescription: state.taskDescription,
|
|
1492
1792
|
lastActivityAt: state.lastActivityAt,
|
|
@@ -1504,14 +1804,7 @@ export class ThreadSupervisor {
|
|
|
1504
1804
|
* contains worker threads.
|
|
1505
1805
|
*/
|
|
1506
1806
|
refreshMainProcessAwareness() {
|
|
1507
|
-
setActiveThreadsSnapshot(this.
|
|
1508
|
-
}
|
|
1509
|
-
/** A chat must never receive another chat's task descriptions or inputs. */
|
|
1510
|
-
getConversationTaskSnapshot(conversationId) {
|
|
1511
|
-
return this.getActiveThreadsSnapshot().filter((item) => {
|
|
1512
|
-
const record = this.executionStore?.get(item.threadId);
|
|
1513
|
-
return record?.caller.kind === "general" && record.caller.ref === conversationId;
|
|
1514
|
-
});
|
|
1807
|
+
setActiveThreadsSnapshot(this.getActiveThreadsSnapshot());
|
|
1515
1808
|
}
|
|
1516
1809
|
/**
|
|
1517
1810
|
* Obs-facing overview of all worker threads (including still-spawning ones),
|
|
@@ -1519,14 +1812,10 @@ export class ThreadSupervisor {
|
|
|
1519
1812
|
* which is the lightweight cross-thread awareness feed (ready workers only).
|
|
1520
1813
|
*/
|
|
1521
1814
|
getThreadsOverview() {
|
|
1522
|
-
const result =
|
|
1523
|
-
...item, engine: formatThreadEngineLabel(item.threadId, this.config),
|
|
1524
|
-
}));
|
|
1815
|
+
const result = [];
|
|
1525
1816
|
for (const [threadId, state] of this.workers) {
|
|
1526
1817
|
result.push({
|
|
1527
1818
|
threadId,
|
|
1528
|
-
kind: state.internalExecution ? "execution" : "conversation",
|
|
1529
|
-
...(state.execution ? { taskId: this.executionStore?.get(state.execution.executionId)?.taskId } : {}),
|
|
1530
1819
|
// A spawning worker with buffered work is already active even before
|
|
1531
1820
|
// its first busy status IPC arrives.
|
|
1532
1821
|
busy: this.isWorkerVisiblyBusy(state),
|