openmausbot 0.1.67 → 0.1.68
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/assets/index-CbaejjQA.js +278 -0
- package/dist/assets/{index-BwpNqgCm.js → index-HqooKFTS.js} +1 -1
- package/dist/assets/index-aJ5EWZOQ.css +1 -0
- package/dist/index.html +2 -2
- package/dist-server/container-mcp.js +2 -0
- package/dist-server/drivers/agents-proxy.js +28 -3
- package/dist-server/enterprise/server/index.js +14587 -0
- package/dist-server/index.js +271 -74
- package/dist-server/openmausbot.js +484 -289
- package/dist-server/pair-cli.js +484 -289
- package/dist-server/server/brand.js +7 -0
- package/dist-server/server/cli.js +23 -1
- package/dist-server/server/config.js +7 -1
- package/dist-server/server/delegations.js +56 -11
- package/dist-server/server/drivers/agents-proxy.js +41 -5
- package/dist-server/server/index.js +194 -60
- package/dist-server/server/routines.js +36 -9
- package/dist-server/server/service-cli.js +63 -0
- package/dist-server/server/service-unit.js +129 -0
- package/dist-server/server/steer-queue.js +2 -1
- package/dist-server/server/store.js +30 -4
- package/dist-server/server/system-prompt.js +1 -0
- package/dist-server/server/turn-dispatch-guard.js +6 -0
- package/dist-server/vps-container-mcp.js +2 -0
- package/enterprise/LICENSE +32 -0
- package/enterprise/README.md +89 -0
- package/enterprise/server/index.js +14587 -0
- package/package.json +2 -1
- package/dist/assets/index-A3CBTxwl.css +0 -1
- package/dist/assets/index-CDuyoVPJ.js +0 -278
|
@@ -33,7 +33,7 @@ import { chiefOfStaffSystemPrompt } from "./chief-of-staff.js";
|
|
|
33
33
|
import { peerAllowed, peerName, peerRosterSystemPrompt, reachablePeers, roomPeerRosterSystemPrompt, roomRosterLine } from "./peer-roster.js";
|
|
34
34
|
import { openMausStatusSystemPrompt } from "./openmaus-status-capsule.js";
|
|
35
35
|
import { containerComputerAction, containerComputerExists, containerComputerFrame, containerComputerMcp, containerComputerScreenshot, containerComputerStatus, containerRuntimeStatus, localVmRecreatableOnDemand, perBotLocalVmTarget, SHARED_LOCAL_VM_TARGET, setupCommands, } from "./container-computer.js";
|
|
36
|
-
import { ensureDirs, instanceConfigs, loadConfig, localVmMaxInstances, localVmMode, parseConfigPatch, roomTurnTimeoutMinutes, saveConfig, showToolCallsEnabled, skillRecorderEnabled, builtInBrowserEnabled, browserProfileReplacementConflict, browserProfilePartitionTarget, syncCredentialEnv, withInstanceCli, persistableInstanceConfigs, vpsSshAlias, DATA_DIR, EVENTS_DIR, NATIVE_DIR, customMcpServers, } from "./config.js";
|
|
36
|
+
import { ensureDirs, instanceConfigs, loadConfig, localVmMaxInstances, localVmMode, parseConfigPatch, roomTurnTimeoutMinutes, maxConcurrentBotThreads, saveConfig, showToolCallsEnabled, skillRecorderEnabled, builtInBrowserEnabled, browserProfileReplacementConflict, browserProfilePartitionTarget, syncCredentialEnv, withInstanceCli, persistableInstanceConfigs, vpsSshAlias, DATA_DIR, EVENTS_DIR, NATIVE_DIR, customMcpServers, } from "./config.js";
|
|
37
37
|
import { ComputerControl } from "./computer-control.js";
|
|
38
38
|
import { MAX_REMOTE_COMMAND_LENGTH } from "./remote-computer.js";
|
|
39
39
|
import { augmentedPath, findCliCandidates, resetPathCache } from "./env-path.js";
|
|
@@ -63,7 +63,7 @@ import { selectDefaultModelSelection } from "./default-model-selection.js";
|
|
|
63
63
|
import { cancelPeerApprovalsFor, cancelPeerApprovalsForThread, dismissStalePeerCards, requestPeerApproval, resolvePeerComms } from "./peer-approval.js";
|
|
64
64
|
import { peerProvenanceNote, withPeerProvenance } from "./peer-provenance.js";
|
|
65
65
|
import { decideRoomPost, emptyRoomPostBudget } from "./room-post-budget.js";
|
|
66
|
-
import { mentionedBots, roomResponders, sectionKey, Store, } from "./store.js";
|
|
66
|
+
import { isProjectEmoji, mentionedBots, roomResponders, sectionKey, Store, } from "./store.js";
|
|
67
67
|
import * as tts from "./tts/index.js";
|
|
68
68
|
import { narrateTool, toUtterances } from "./tts/speech-text.js";
|
|
69
69
|
import { buildTurnContext, engineIsFresh } from "./turn-context.js";
|
|
@@ -78,7 +78,7 @@ import { fetchSkillFromSource } from "./skill-fetch.js";
|
|
|
78
78
|
import { expandLearnTurnText, learnSource } from "./skill-learn.js";
|
|
79
79
|
import { expandSetupTurnText, setupModeActive, setupSystemPrompt } from "./setup-mode.js";
|
|
80
80
|
import { checkSoulDrift, readSoulDrift, soulFile, writeSoulMirror } from "./bot-folder.js";
|
|
81
|
-
import { buildSystemPrompt, computerPrompt, mentionPrompt, COMPOSIO_PROMPT, CREDENTIAL_PROMPT, LEARN_PROMPT, PROFILE_PROMPT, ROUTINE_PROMPT, WEBHOOK_PROMPT, } from "./system-prompt.js";
|
|
81
|
+
import { buildSystemPrompt, computerPrompt, mentionPrompt, COMPOSIO_PROMPT, CREDENTIAL_PROMPT, LEARN_PROMPT, PROFILE_PROMPT, ROUTINE_PROMPT, ROUTINE_EXECUTION_PROMPT, WEBHOOK_PROMPT, } from "./system-prompt.js";
|
|
82
82
|
import { readCuaConnection, gatedLocalComputer } from "./local-computer.js";
|
|
83
83
|
import { discoverExistingPerBotLocalVms, localVmInventoryEntry, shouldArmLocalVmIdle, } from "./local-vm-inventory.js";
|
|
84
84
|
import { LocalVmIdleTimer } from "./local-vm-idle.js";
|
|
@@ -116,7 +116,7 @@ import { createTeamBackup, importTeamBackup } from "./team-backup.js";
|
|
|
116
116
|
import { MAX_TEAM_BACKUP_BYTES } from "../shared/team-backup.js";
|
|
117
117
|
import { shouldMountLocalComputer } from "./local-routing.js";
|
|
118
118
|
import { resolveSurface } from "./surface.js";
|
|
119
|
-
import { PendingTurnCancellations, ProviderTurnGenerationRegistry, RetiredTurnRegistry, guardTurnDispatch, isTurnEventQuarantined, } from "./turn-dispatch-guard.js";
|
|
119
|
+
import { PendingTurnCancellations, ProviderTurnGenerationRegistry, RetiredTurnRegistry, guardTurnDispatch, isTurnAdmissionBlocked, isTurnEventQuarantined, } from "./turn-dispatch-guard.js";
|
|
120
120
|
import { createGracefulShutdown } from "./graceful-shutdown.js";
|
|
121
121
|
import { acquireDataDirLeaseForProcess } from "./data-dir-lease.js";
|
|
122
122
|
import { describeEdition, editionStatus, loadEnterpriseLayer } from "./enterprise.js";
|
|
@@ -448,7 +448,6 @@ const directTurnGenerationByThread = new Map();
|
|
|
448
448
|
// Keep the exact provider/profile settings that own a running conversation.
|
|
449
449
|
// Selecting another thread or changing a default must not retarget its tools.
|
|
450
450
|
const directTurnBots = new Map();
|
|
451
|
-
const MAX_CONCURRENT_BOT_THREADS = 3;
|
|
452
451
|
let providerFleetReloading = false;
|
|
453
452
|
const turnResources = new TurnResources();
|
|
454
453
|
const turnResourceOwners = new Map();
|
|
@@ -484,6 +483,11 @@ function botForThread(botId, threadId) {
|
|
|
484
483
|
function threadBusy(botId, threadId) {
|
|
485
484
|
return store.taskByThread(botId, threadId)?.busy === true || directTurnDispatchClaims.has(threadId);
|
|
486
485
|
}
|
|
486
|
+
function botAtThreadCapacity(botId) {
|
|
487
|
+
// Setup/dispatch reservations still occupy a slot even if an early
|
|
488
|
+
// completion event has already cleared the stored busy flag.
|
|
489
|
+
return store.tasks(botId).filter((task) => threadBusy(botId, task.threadId)).length >= maxConcurrentBotThreads(cfg);
|
|
490
|
+
}
|
|
487
491
|
function hasDirectDispatch(botId) {
|
|
488
492
|
return [...directTurnDispatchClaims.values()].some((claim) => claim.botId === botId);
|
|
489
493
|
}
|
|
@@ -1494,6 +1498,10 @@ function finishGroupGoalRun(groupId, operation, status, detail) {
|
|
|
1494
1498
|
// A calendar-triggered team goal reuses its RoutineRun id for this card.
|
|
1495
1499
|
// Manual goals have unrelated ids, so the manager safely ignores them.
|
|
1496
1500
|
const routineRun = routines?.finishGoalRun(run.runId, status, safeDetail);
|
|
1501
|
+
if (routineRun?.status === "cancelled") {
|
|
1502
|
+
pendingDelegationWakes.delete(operation.threadId);
|
|
1503
|
+
discardDelegations(commsBus, operation.threadId);
|
|
1504
|
+
}
|
|
1497
1505
|
// Member-level turn completions are intentionally private/intermediate for
|
|
1498
1506
|
// a team goal, so the normal direct-routine notification path never fires.
|
|
1499
1507
|
// Notify once from the correlated terminal receipt instead.
|
|
@@ -1502,13 +1510,15 @@ function finishGroupGoalRun(groupId, operation, status, detail) {
|
|
|
1502
1510
|
if (routineRun?.status === "waiting") {
|
|
1503
1511
|
const coordinator = store.bot(routineRun.botId);
|
|
1504
1512
|
if (coordinator) {
|
|
1505
|
-
|
|
1513
|
+
const notificationBot = routineSourceOwner(routineRun)?.bot ?? coordinator;
|
|
1514
|
+
notify(buildNotification("question", notificationBot, routineSourceThread(routineRun) ?? routineRun.threadId ?? operation.threadId, safeDetail || `${routineRun.routineName} needs your input`, { avatarUrl: notificationBot.avatarUrl }));
|
|
1506
1515
|
}
|
|
1507
1516
|
}
|
|
1508
1517
|
if (routineRun?.status === "completed") {
|
|
1509
1518
|
const coordinator = store.bot(routineRun.botId);
|
|
1510
1519
|
if (coordinator) {
|
|
1511
|
-
|
|
1520
|
+
const notificationBot = routineSourceOwner(routineRun)?.bot ?? coordinator;
|
|
1521
|
+
notify(buildNotification("done", notificationBot, routineSourceThread(routineRun) ?? routineRun.threadId ?? operation.threadId, safeDetail || routineRun.routineName, { avatarUrl: notificationBot.avatarUrl }));
|
|
1512
1522
|
}
|
|
1513
1523
|
}
|
|
1514
1524
|
const group = store.group(groupId);
|
|
@@ -2033,9 +2043,17 @@ const watchdog = new TurnWatchdog({
|
|
|
2033
2043
|
revokeInternalCapabilitiesForThread(turn.threadId);
|
|
2034
2044
|
repeats.settle(turn.threadId);
|
|
2035
2045
|
const bot = botForThread(turn.botId, turn.threadId);
|
|
2036
|
-
const
|
|
2046
|
+
const routineRun = activeRoutineRunForThread(turn.threadId);
|
|
2047
|
+
const instance = routineRun?.runOn === "cloud"
|
|
2048
|
+
? registry.instances().find((candidate) => candidate.driverKind === "boxAgent")
|
|
2049
|
+
: bot ? registry.get(bot.modelSelection.instanceId) : null;
|
|
2037
2050
|
void instance?.adapter.interruptTurn(turn.threadId).catch(() => { });
|
|
2038
2051
|
const minutes = Math.round(TURN_STALL_MS / 60_000);
|
|
2052
|
+
if (routineRun?.target === "bot") {
|
|
2053
|
+
routines?.failThread(turn.threadId, `No activity for ${minutes} minutes — the routine was stopped`);
|
|
2054
|
+
pendingDelegationWakes.delete(turn.threadId);
|
|
2055
|
+
discardDelegations(commsBus, turn.threadId);
|
|
2056
|
+
}
|
|
2039
2057
|
store.appendMessage(turn.threadId, {
|
|
2040
2058
|
role: "bot",
|
|
2041
2059
|
kind: "activity",
|
|
@@ -2826,7 +2844,8 @@ bus.subscribe((event) => {
|
|
|
2826
2844
|
store.setTaskActivity(bot.id, event.threadId, "waiting-on-you");
|
|
2827
2845
|
else if (asker.busy)
|
|
2828
2846
|
store.setActivity(asker.id, "waiting-on-you");
|
|
2829
|
-
|
|
2847
|
+
const notificationBot = (routineRun && routineSourceOwner(routineRun)?.bot) || asker;
|
|
2848
|
+
notify(buildNotification(permission ? "approval" : "question", notificationBot, (routineRun && routineSourceThread(routineRun)) || event.threadId, event.summary));
|
|
2830
2849
|
};
|
|
2831
2850
|
if (reviewTask && reviewMode === "enforce") {
|
|
2832
2851
|
// Avoid buzzing the owner for a card the reviewer is about to answer.
|
|
@@ -2991,13 +3010,14 @@ bus.subscribe((event) => {
|
|
|
2991
3010
|
// A failed peer turn stays a chip too. The bot that delegated is woken
|
|
2992
3011
|
// with the failure and answers the person in its own thread — buzzing
|
|
2993
3012
|
// here as well would ring twice for one piece of news.
|
|
2994
|
-
if (routineRun
|
|
3013
|
+
if ((!routineRun || routineRun.status === "completed") && !internal) {
|
|
2995
3014
|
// the frame carries the bot's avatar so every desktop client can
|
|
2996
3015
|
// show the notification under that bot's own face
|
|
2997
3016
|
const completionDetail = routineRun
|
|
2998
3017
|
? reply || routineRun.output || routineRun.routineName
|
|
2999
3018
|
: reply;
|
|
3000
|
-
|
|
3019
|
+
const notificationBot = (routineRun && routineSourceOwner(routineRun)?.bot) || bot;
|
|
3020
|
+
notify(buildNotification("done", notificationBot, routineReportThread ?? event.threadId, completionDetail, { avatarUrl: notificationBot.avatarUrl }));
|
|
3001
3021
|
}
|
|
3002
3022
|
if (screenPollers.has(event.threadId)) {
|
|
3003
3023
|
if (resourceOwner)
|
|
@@ -3064,9 +3084,22 @@ const delegationWatch = new Map();
|
|
|
3064
3084
|
// the cardContinuation resume pattern used for connector/credential cards.
|
|
3065
3085
|
const delegationWakeBudget = new DelegationWakeBudget();
|
|
3066
3086
|
const pendingDelegationWakes = new Map();
|
|
3067
|
-
function
|
|
3087
|
+
function activeRoutineRunForThread(threadId) {
|
|
3088
|
+
const run = routines?.runForThread(threadId);
|
|
3089
|
+
return run && ["running", "waiting"].includes(run.status) ? run : null;
|
|
3090
|
+
}
|
|
3091
|
+
function routineDelegationCanResume(threadId, routineRunId) {
|
|
3092
|
+
return !routineRunId || activeRoutineRunForThread(threadId)?.id === routineRunId;
|
|
3093
|
+
}
|
|
3094
|
+
function dispatchDelegationWake(botId, threadId, targetName, failureReason, routineRunId, budgetAcquired = false) {
|
|
3068
3095
|
if (!store.taskByThread(botId, threadId))
|
|
3069
3096
|
return;
|
|
3097
|
+
if (!routineDelegationCanResume(threadId, routineRunId))
|
|
3098
|
+
return;
|
|
3099
|
+
if (!budgetAcquired && !delegationWakeBudget.tryAcquire(threadId)) {
|
|
3100
|
+
routines?.failThread(threadId, "Delegation follow-up limit reached; review the run before retrying");
|
|
3101
|
+
return;
|
|
3102
|
+
}
|
|
3070
3103
|
const prompt = failureReason
|
|
3071
3104
|
? buildDelegationFailurePrompt(targetName, failureReason)
|
|
3072
3105
|
: buildDelegationRevivalPrompt(targetName);
|
|
@@ -3081,8 +3114,9 @@ function dispatchDelegationWake(botId, threadId, targetName, failureReason) {
|
|
|
3081
3114
|
return;
|
|
3082
3115
|
const message = error instanceof Error ? error.message : String(error);
|
|
3083
3116
|
// Raced with a user turn claiming the bot — retry once it settles.
|
|
3084
|
-
|
|
3085
|
-
|
|
3117
|
+
// This is the same logical wake, so keep its original budget charge.
|
|
3118
|
+
if (isTurnAdmissionBlocked(error)) {
|
|
3119
|
+
pendingDelegationWakes.set(threadId, { botId, targetName, failureReason, routineRunId, budgetAcquired: true });
|
|
3086
3120
|
return;
|
|
3087
3121
|
}
|
|
3088
3122
|
store.appendMessage(threadId, {
|
|
@@ -3093,37 +3127,43 @@ function dispatchDelegationWake(botId, threadId, targetName, failureReason) {
|
|
|
3093
3127
|
ok: false,
|
|
3094
3128
|
},
|
|
3095
3129
|
});
|
|
3130
|
+
routines?.failThread(threadId, `Could not resume after delegation: ${message}`);
|
|
3096
3131
|
});
|
|
3097
3132
|
}
|
|
3098
|
-
function wakeDelegationSource(source, threadId, targetName, failureReason) {
|
|
3133
|
+
function wakeDelegationSource(source, threadId, targetName, failureReason, routineRunId) {
|
|
3099
3134
|
if (!store.taskByThread(source.id, threadId))
|
|
3100
3135
|
return;
|
|
3136
|
+
if (!routineDelegationCanResume(threadId, routineRunId))
|
|
3137
|
+
return;
|
|
3101
3138
|
// Busy? Hold the wake until the source settles, then drain it — the
|
|
3102
3139
|
// delegated reply is already in the thread, so nothing is lost, and the
|
|
3103
3140
|
// source processes it the moment it is free rather than only on a later
|
|
3104
3141
|
// user nudge.
|
|
3105
3142
|
if (threadBusy(source.id, threadId) || activeGroupTurnForBot(source.id)) {
|
|
3106
|
-
pendingDelegationWakes.set(threadId, { botId: source.id, targetName, failureReason });
|
|
3143
|
+
pendingDelegationWakes.set(threadId, { botId: source.id, targetName, failureReason, routineRunId });
|
|
3107
3144
|
return;
|
|
3108
3145
|
}
|
|
3109
|
-
|
|
3110
|
-
return;
|
|
3111
|
-
dispatchDelegationWake(source.id, threadId, targetName, failureReason);
|
|
3146
|
+
dispatchDelegationWake(source.id, threadId, targetName, failureReason, routineRunId);
|
|
3112
3147
|
}
|
|
3113
3148
|
function drainDelegationWakes() {
|
|
3114
3149
|
for (const [threadId, entry] of pendingDelegationWakes) {
|
|
3115
|
-
if (!store.taskByThread(entry.botId, threadId)) {
|
|
3150
|
+
if (!store.taskByThread(entry.botId, threadId) || !routineDelegationCanResume(threadId, entry.routineRunId)) {
|
|
3116
3151
|
pendingDelegationWakes.delete(threadId);
|
|
3117
3152
|
continue;
|
|
3118
3153
|
}
|
|
3119
3154
|
if (threadBusy(entry.botId, threadId) || activeGroupTurnForBot(entry.botId))
|
|
3120
3155
|
continue;
|
|
3121
3156
|
pendingDelegationWakes.delete(threadId);
|
|
3122
|
-
|
|
3123
|
-
continue;
|
|
3124
|
-
dispatchDelegationWake(entry.botId, threadId, entry.targetName, entry.failureReason);
|
|
3157
|
+
dispatchDelegationWake(entry.botId, threadId, entry.targetName, entry.failureReason, entry.routineRunId, entry.budgetAcquired);
|
|
3125
3158
|
}
|
|
3126
3159
|
}
|
|
3160
|
+
function wakeUndispatchedDelegation(receipt, routineRunId) {
|
|
3161
|
+
const source = store.botByThread(receipt.sourceThreadId);
|
|
3162
|
+
if (!source)
|
|
3163
|
+
return;
|
|
3164
|
+
markTaskContextExternallyUpdated(source, receipt.sourceThreadId);
|
|
3165
|
+
wakeDelegationSource(source, receipt.sourceThreadId, receipt.toBotName, receipt.result || "the handoff did not run", routineRunId);
|
|
3166
|
+
}
|
|
3127
3167
|
// Provider-native sessions only know about messages produced inside their
|
|
3128
3168
|
// own turns. A delegated result is appended later by the harness, so mark the
|
|
3129
3169
|
// source task with a persisted, impossible-to-resume owner. Its next turn
|
|
@@ -3255,11 +3295,11 @@ function finalizeDelegationWatch(threadId, ok, reply = "", failureName = "Delega
|
|
|
3255
3295
|
// silent" gap). Failures wake it too — the user must hear the task did
|
|
3256
3296
|
// not finish. Idle-checked and burst-capped so a busy source or a
|
|
3257
3297
|
// re-delegating loop cannot spin up runs.
|
|
3258
|
-
if (ok
|
|
3259
|
-
wakeDelegationSource(source, watched.sourceThreadId, targetName);
|
|
3298
|
+
if (ok) {
|
|
3299
|
+
wakeDelegationSource(source, watched.sourceThreadId, targetName, undefined, watched.routineRunId);
|
|
3260
3300
|
}
|
|
3261
3301
|
else if (!ok) {
|
|
3262
|
-
wakeDelegationSource(source, watched.sourceThreadId, targetName, failureName || "the delegated turn did not finish");
|
|
3302
|
+
wakeDelegationSource(source, watched.sourceThreadId, targetName, failureName || "the delegated turn did not finish", watched.routineRunId);
|
|
3263
3303
|
}
|
|
3264
3304
|
}
|
|
3265
3305
|
}
|
|
@@ -3329,6 +3369,7 @@ const runDelegatedTurn = (toBotId, text, commsDepth, sourceThreadId, channel, ta
|
|
|
3329
3369
|
taskId,
|
|
3330
3370
|
sourceThreadId,
|
|
3331
3371
|
sourceBotId,
|
|
3372
|
+
routineRunId: activeRoutineRunForThread(sourceThreadId)?.id,
|
|
3332
3373
|
startedAtMs: Date.now(),
|
|
3333
3374
|
});
|
|
3334
3375
|
}
|
|
@@ -3365,6 +3406,10 @@ const runDelegatedTurn = (toBotId, text, commsDepth, sourceThreadId, channel, ta
|
|
|
3365
3406
|
reportStartFailure(err);
|
|
3366
3407
|
});
|
|
3367
3408
|
};
|
|
3409
|
+
function drainThreadDelegations(threadId) {
|
|
3410
|
+
const routineRunId = activeRoutineRunForThread(threadId)?.id;
|
|
3411
|
+
drainDelegations(commsBus, approvalBus, threadId, runDelegatedTurn, (receipt) => wakeUndispatchedDelegation(receipt, routineRunId));
|
|
3412
|
+
}
|
|
3368
3413
|
// Most waiting handoffs retry from a target's turn.completed event. Some
|
|
3369
3414
|
// setup, cancellation, room, watchdog, and provider-reload paths release a
|
|
3370
3415
|
// bot without that event, so every explicit idle release calls this same
|
|
@@ -3377,10 +3422,13 @@ function retryDelegationsWaitingOn(botId) {
|
|
|
3377
3422
|
delegationRetryBots.add(botId);
|
|
3378
3423
|
queueMicrotask(() => {
|
|
3379
3424
|
delegationRetryBots.delete(botId);
|
|
3425
|
+
// Explicit idle releases (room/setup/reload/watchdog fallbacks) may not
|
|
3426
|
+
// publish turn.completed. They free a waiting source continuation too.
|
|
3427
|
+
drainDelegationWakes();
|
|
3380
3428
|
if (store.bot(botId)?.busy)
|
|
3381
3429
|
return;
|
|
3382
3430
|
for (const waitingThread of releaseDelegationsWaitingOn(botId)) {
|
|
3383
|
-
|
|
3431
|
+
drainThreadDelegations(waitingThread);
|
|
3384
3432
|
}
|
|
3385
3433
|
});
|
|
3386
3434
|
}
|
|
@@ -3395,7 +3443,7 @@ bus.subscribe((event) => {
|
|
|
3395
3443
|
if (!event.ok)
|
|
3396
3444
|
discardDelegations(commsBus, event.threadId);
|
|
3397
3445
|
else
|
|
3398
|
-
|
|
3446
|
+
drainThreadDelegations(event.threadId);
|
|
3399
3447
|
// A settling bot frees itself as a delegation TARGET too: handoffs that
|
|
3400
3448
|
// found it busy earlier were kept queued (bounded retries) on their own
|
|
3401
3449
|
// source threads, and this is the moment they get their retry.
|
|
@@ -3441,7 +3489,24 @@ function drainQueuedSends() {
|
|
|
3441
3489
|
}),
|
|
3442
3490
|
// Provider completion can precede its dispatch promise: keep the queue
|
|
3443
3491
|
// intact until that exact handshake releases its runtime-only claim.
|
|
3444
|
-
(botId, threadId) => threadBusy(botId, threadId) || Boolean(activeGroupTurnForBot(botId)));
|
|
3492
|
+
(botId, threadId) => threadBusy(botId, threadId) || botAtThreadCapacity(botId) || Boolean(activeGroupTurnForBot(botId)));
|
|
3493
|
+
}
|
|
3494
|
+
/** Keep a person's words off the transcript until a direct-thread slot is
|
|
3495
|
+
* available. Reuse the existing cancellable, idempotent composer queue. */
|
|
3496
|
+
async function startOrQueueDirectMessage(botId, threadId, text, replyTo, sendId) {
|
|
3497
|
+
const capacity = botAtThreadCapacity(botId);
|
|
3498
|
+
if (capacity || threadBusy(botId, threadId)) {
|
|
3499
|
+
const reason = capacity ? "capacity" : undefined;
|
|
3500
|
+
const queued = queueSteeredMessage(botId, threadId, text, {
|
|
3501
|
+
replyToId: replyTo?.id,
|
|
3502
|
+
sendId,
|
|
3503
|
+
reason,
|
|
3504
|
+
prompt: promptWithReply(text, replyTo, cfg.profile?.name?.trim() || "User"),
|
|
3505
|
+
});
|
|
3506
|
+
return { ok: true, queued: true, queueId: queued.id, threadId, reason };
|
|
3507
|
+
}
|
|
3508
|
+
const message = await startTurn(botId, text, { threadId, replyTo, sendId });
|
|
3509
|
+
return { ok: true, threadId, message };
|
|
3445
3510
|
}
|
|
3446
3511
|
const screenPollers = new Map();
|
|
3447
3512
|
/** The preview shares the box's single command endpoint with the agent's
|
|
@@ -3576,6 +3641,19 @@ async function startTurn(botId, text, opts) {
|
|
|
3576
3641
|
if (!profile)
|
|
3577
3642
|
throw Object.assign(new Error("no such bot"), { status: 404 });
|
|
3578
3643
|
const threadId = opts?.threadId ?? profile.threadId;
|
|
3644
|
+
const continuingRoutine = opts?.cardContinuation ? activeRoutineRunForThread(threadId) : null;
|
|
3645
|
+
if (continuingRoutine) {
|
|
3646
|
+
const onDispatchError = opts?.onDispatchError;
|
|
3647
|
+
opts = {
|
|
3648
|
+
...opts,
|
|
3649
|
+
runOn: continuingRoutine.runOn,
|
|
3650
|
+
automationSource: continuingRoutine.triggerSource ?? (continuingRoutine.manual ? "manual" : "schedule"),
|
|
3651
|
+
onDispatchError: (message) => {
|
|
3652
|
+
routines?.failThread(threadId, message);
|
|
3653
|
+
onDispatchError?.(message);
|
|
3654
|
+
},
|
|
3655
|
+
};
|
|
3656
|
+
}
|
|
3579
3657
|
const bot = store.projectBotForTask(botId, threadId);
|
|
3580
3658
|
if (!bot)
|
|
3581
3659
|
throw Object.assign(new Error("no such task"), { status: 404 });
|
|
@@ -3600,8 +3678,8 @@ async function startTurn(botId, text, opts) {
|
|
|
3600
3678
|
if (activeGroupTurnForBot(botId)) {
|
|
3601
3679
|
throw Object.assign(new Error("the bot is already working in a channel — wait for it to finish"), { status: 409, code: "thread_busy" });
|
|
3602
3680
|
}
|
|
3603
|
-
if (
|
|
3604
|
-
throw Object.assign(new Error(
|
|
3681
|
+
if (botAtThreadCapacity(botId)) {
|
|
3682
|
+
throw Object.assign(new Error(`this bot has reached its limit of ${maxConcurrentBotThreads(cfg)} parallel threads — wait for one to finish`), { status: 409, code: "thread_limit" });
|
|
3605
3683
|
}
|
|
3606
3684
|
// Retire anything a previous turn left behind before minting this turn's
|
|
3607
3685
|
// integrations. Completion and interrupt paths do the same; this is the
|
|
@@ -4169,6 +4247,7 @@ async function startTurn(botId, text, opts) {
|
|
|
4169
4247
|
{ id: "credential", label: "Credentials", text: credentialPrompt },
|
|
4170
4248
|
{ id: "recall", label: "Recall", text: recallPrompt },
|
|
4171
4249
|
{ id: "routine", label: "Routines", text: routinePrompt },
|
|
4250
|
+
{ id: "routine-execution", label: "Routine execution", text: opts?.automationSource === "schedule" || opts?.automationSource === "manual" ? ROUTINE_EXECUTION_PROMPT : "" },
|
|
4172
4251
|
{ id: "profile", label: "Profile changes", text: profilePrompt },
|
|
4173
4252
|
{ id: "learn", label: "Skill authoring", text: learnPrompt },
|
|
4174
4253
|
{ id: "section-context", label: "Section context", text: sectionContextSystemPrompt(bot.section) },
|
|
@@ -4309,8 +4388,14 @@ function routineSourceOwner(run) {
|
|
|
4309
4388
|
const bot = store.bot(run.botId);
|
|
4310
4389
|
if (!bot)
|
|
4311
4390
|
return null;
|
|
4312
|
-
|
|
4313
|
-
|
|
4391
|
+
// The source is stamped from the confirmed request, never calendar input.
|
|
4392
|
+
// A request for a teammate runs as that teammate but reports to the bot
|
|
4393
|
+
// whose conversation held the card. Recheck their section before sharing
|
|
4394
|
+
// a result, since either bot may have moved since confirmation.
|
|
4395
|
+
const sourceBot = store.botByThread(threadId);
|
|
4396
|
+
if (sourceBot && sectionKey(sourceBot.section) === sectionKey(bot.section)) {
|
|
4397
|
+
return { bot: sourceBot, group: undefined, threadId };
|
|
4398
|
+
}
|
|
4314
4399
|
const group = store.groupByThread(threadId);
|
|
4315
4400
|
return group?.memberIds.includes(bot.id) ? { bot, group, threadId } : null;
|
|
4316
4401
|
}
|
|
@@ -4450,8 +4535,16 @@ async function stopBotForEmergencyApprovalDowngrade(botId) {
|
|
|
4450
4535
|
}
|
|
4451
4536
|
await directStop;
|
|
4452
4537
|
}
|
|
4538
|
+
// Load queued handoffs before scheduler recovery can fail an interrupted
|
|
4539
|
+
// run. Its failure callback can then durably drop that work immediately;
|
|
4540
|
+
// nothing dispatches until the listener is ready below.
|
|
4541
|
+
const commsBus = { store, broadcast };
|
|
4542
|
+
_loadPending();
|
|
4453
4543
|
routines = new RoutineManager({
|
|
4454
4544
|
emit: broadcast,
|
|
4545
|
+
hasPendingDelegations: (threadId) => pendingThreads().includes(threadId) ||
|
|
4546
|
+
[...delegationWatch.values()].some((watch) => watch.sourceThreadId === threadId) ||
|
|
4547
|
+
pendingDelegationWakes.has(threadId),
|
|
4455
4548
|
botState: (botId) => {
|
|
4456
4549
|
const bot = store.bot(botId);
|
|
4457
4550
|
return !bot ? "missing" : bot.busy ? "busy" : "ready";
|
|
@@ -4488,6 +4581,8 @@ routines = new RoutineManager({
|
|
|
4488
4581
|
},
|
|
4489
4582
|
interruptTurn: async (botId, threadId, runOn) => {
|
|
4490
4583
|
const bot = botForThread(botId, threadId);
|
|
4584
|
+
pendingDelegationWakes.delete(threadId);
|
|
4585
|
+
discardDelegations(commsBus, threadId);
|
|
4491
4586
|
cancelDirectTurnDispatch(botId, threadId);
|
|
4492
4587
|
revokeInternalCapabilitiesForThread(threadId);
|
|
4493
4588
|
const instance = runOn === "cloud"
|
|
@@ -4505,11 +4600,16 @@ routines = new RoutineManager({
|
|
|
4505
4600
|
interruptGoal: interruptRoutineGroupGoal,
|
|
4506
4601
|
onRunChanged: syncRoutineRunToSource,
|
|
4507
4602
|
onRunFailed: (run) => {
|
|
4603
|
+
if (run.threadId) {
|
|
4604
|
+
pendingDelegationWakes.delete(run.threadId);
|
|
4605
|
+
discardDelegations(commsBus, run.threadId);
|
|
4606
|
+
}
|
|
4508
4607
|
const bot = store.bot(run.botId);
|
|
4509
4608
|
if (!bot)
|
|
4510
4609
|
return;
|
|
4511
4610
|
const detail = run.error ? `${run.routineName}: ${run.error}` : run.routineName;
|
|
4512
|
-
|
|
4611
|
+
const notificationBot = routineSourceOwner(run)?.bot ?? bot;
|
|
4612
|
+
notify(buildNotification("routine-failed", notificationBot, routineSourceThread(run) ?? run.threadId ?? bot.threadId, detail));
|
|
4513
4613
|
},
|
|
4514
4614
|
});
|
|
4515
4615
|
// The scheduler receipt and room transcript live in separate durable stores.
|
|
@@ -4551,7 +4651,6 @@ if (recoveryOwners.length > 0) {
|
|
|
4551
4651
|
: [];
|
|
4552
4652
|
})));
|
|
4553
4653
|
}
|
|
4554
|
-
routines.start();
|
|
4555
4654
|
// Chat tools can prepare routine changes, but the harness applies them only
|
|
4556
4655
|
// after the user confirms a durable card. Keeping this beside the scheduler
|
|
4557
4656
|
// makes the card resolvable after an app restart without involving the model.
|
|
@@ -4806,10 +4905,6 @@ function serializeRoomContext(threadId, userName, textOverride, readerBotId) {
|
|
|
4806
4905
|
})
|
|
4807
4906
|
.join("\n");
|
|
4808
4907
|
}
|
|
4809
|
-
// comms bus: passed into the visibility helpers in comms-visibility.ts so
|
|
4810
|
-
// they can mirror messages + chips without re-deriving SSE plumbing. Same
|
|
4811
|
-
// shape every comms entry point uses (ask_bot, delegate_bot).
|
|
4812
|
-
const commsBus = { store, broadcast };
|
|
4813
4908
|
// What each room has already taken from its bots. Keyed by room because the
|
|
4814
4909
|
// loop post_to_room can start is a property of the room, not of any one
|
|
4815
4910
|
// caller — three bots posting twice each is the same runaway as one bot
|
|
@@ -4830,18 +4925,6 @@ const approvalBus = { store, broadcast, notify };
|
|
|
4830
4925
|
if (stale)
|
|
4831
4926
|
console.log(`peer approvals: dismissed ${stale} card(s) left by a previous run`);
|
|
4832
4927
|
}
|
|
4833
|
-
// Handoffs a previous process queued but never ran: the source turn is
|
|
4834
|
-
// dead (no turn survives a restart) so they would otherwise wait forever.
|
|
4835
|
-
// Run them now, through the same drain — target and approvePeerComms are
|
|
4836
|
-
// re-checked there as always; a source bot that no longer exists is skipped.
|
|
4837
|
-
_loadPending();
|
|
4838
|
-
{
|
|
4839
|
-
const leftover = pendingThreads();
|
|
4840
|
-
if (leftover.length)
|
|
4841
|
-
console.log(`delegations: ${leftover.length} thread(s) with queued handoffs from a previous run — draining`);
|
|
4842
|
-
for (const threadId of leftover)
|
|
4843
|
-
drainDelegations(commsBus, approvalBus, threadId, runDelegatedTurn);
|
|
4844
|
-
}
|
|
4845
4928
|
async function runGroupMemberTurn(groupId, threadId, botId, hop,
|
|
4846
4929
|
// bots that already spoke for this user message — "@Scout ask @Pixel"
|
|
4847
4930
|
// must not run Pixel twice (once chained, once as a direct responder)
|
|
@@ -6402,7 +6485,7 @@ function dispatchConnectorResume(entry) {
|
|
|
6402
6485
|
onDispatchError: (message) => markConnectorResumeFailed(entry.threadId, entry.resumeKey, message),
|
|
6403
6486
|
}).catch((error) => {
|
|
6404
6487
|
const message = error instanceof Error ? error.message : String(error);
|
|
6405
|
-
if (
|
|
6488
|
+
if (isTurnAdmissionBlocked(error))
|
|
6406
6489
|
pendingConnectorResumes.set(`${entry.threadId}:${entry.resumeKey}`, entry);
|
|
6407
6490
|
else
|
|
6408
6491
|
markConnectorResumeFailed(entry.threadId, entry.resumeKey, message);
|
|
@@ -6529,7 +6612,7 @@ function dispatchSecretResume(entry) {
|
|
|
6529
6612
|
onDispatchError: (message) => markSecretResumeFailed(entry.threadId, entry.messageId, message),
|
|
6530
6613
|
}).catch((error) => {
|
|
6531
6614
|
const message = error instanceof Error ? error.message : String(error);
|
|
6532
|
-
if (
|
|
6615
|
+
if (isTurnAdmissionBlocked(error)) {
|
|
6533
6616
|
pendingSecretResumes.set(`${entry.threadId}:${entry.messageId}`, entry);
|
|
6534
6617
|
}
|
|
6535
6618
|
else {
|
|
@@ -6825,6 +6908,7 @@ function configStatus() {
|
|
|
6825
6908
|
// not a secret — the settings picker shows it; "" = follow the system
|
|
6826
6909
|
language: cfg.language ?? "",
|
|
6827
6910
|
rooms: { turnTimeoutMinutes: roomTurnTimeoutMinutes(cfg) },
|
|
6911
|
+
threads: { maxConcurrentPerBot: maxConcurrentBotThreads(cfg) },
|
|
6828
6912
|
localVm: {
|
|
6829
6913
|
mode: localVmMode(cfg),
|
|
6830
6914
|
maxInstances: localVmMaxInstances(cfg),
|
|
@@ -7186,6 +7270,11 @@ const handleRequest = async (req, res) => {
|
|
|
7186
7270
|
if (method === "GET" && path === "/api/health" && !gate.auth) {
|
|
7187
7271
|
return json(res, 200, { app: "openmausbot" });
|
|
7188
7272
|
}
|
|
7273
|
+
// The brand is public too: the sign-in page must carry the deployment's
|
|
7274
|
+
// name and icon before anyone has a session, and it holds nothing secret.
|
|
7275
|
+
if (method === "GET" && path === "/api/brand" && !gate.auth) {
|
|
7276
|
+
return json(res, 200, loadBrand());
|
|
7277
|
+
}
|
|
7189
7278
|
if (!gate.auth)
|
|
7190
7279
|
return json(res, gate.status, { error: gate.error });
|
|
7191
7280
|
const auth = gate.auth;
|
|
@@ -7872,6 +7961,7 @@ const handleRequest = async (req, res) => {
|
|
|
7872
7961
|
taskId,
|
|
7873
7962
|
sourceThreadId: fromThreadId,
|
|
7874
7963
|
sourceBotId: currentFrom.id,
|
|
7964
|
+
routineRunId: activeRoutineRunForThread(fromThreadId)?.id,
|
|
7875
7965
|
});
|
|
7876
7966
|
store.appendMessage(fromThreadId, {
|
|
7877
7967
|
role: "bot",
|
|
@@ -11094,7 +11184,7 @@ const handleRequest = async (req, res) => {
|
|
|
11094
11184
|
if (queued.text !== text || queued.replyToId !== replyTo?.id) {
|
|
11095
11185
|
throw Object.assign(new Error("sendId already belongs to another message"), { status: 409 });
|
|
11096
11186
|
}
|
|
11097
|
-
return { ok: true, queued: true, queueId: queued.id, threadId };
|
|
11187
|
+
return { ok: true, queued: true, queueId: queued.id, threadId, reason: queued.reason };
|
|
11098
11188
|
}
|
|
11099
11189
|
}
|
|
11100
11190
|
const currentAtStart = store.projectBotForTask(bot.id, threadId);
|
|
@@ -11155,8 +11245,7 @@ const handleRequest = async (req, res) => {
|
|
|
11155
11245
|
return { ok: true, steered: true, threadId, message };
|
|
11156
11246
|
}
|
|
11157
11247
|
if (!current.busy) {
|
|
11158
|
-
|
|
11159
|
-
return { ok: true, threadId, message };
|
|
11248
|
+
return startOrQueueDirectMessage(bot.id, threadId, text, replyTo, sendId);
|
|
11160
11249
|
}
|
|
11161
11250
|
const queued = queueSteeredMessage(current.id, threadId, text, {
|
|
11162
11251
|
replyToId: replyTo?.id,
|
|
@@ -11165,8 +11254,7 @@ const handleRequest = async (req, res) => {
|
|
|
11165
11254
|
});
|
|
11166
11255
|
return { ok: true, queued: true, queueId: queued.id, threadId };
|
|
11167
11256
|
}
|
|
11168
|
-
|
|
11169
|
-
return { ok: true, threadId, message };
|
|
11257
|
+
return startOrQueueDirectMessage(bot.id, threadId, text, replyTo, sendId);
|
|
11170
11258
|
});
|
|
11171
11259
|
return json(res, 202, receipt);
|
|
11172
11260
|
}
|
|
@@ -11437,6 +11525,22 @@ const handleRequest = async (req, res) => {
|
|
|
11437
11525
|
});
|
|
11438
11526
|
// Folders organize one bot's threads; they never own settings,
|
|
11439
11527
|
// transcripts or working directories. The project wire names stay stable.
|
|
11528
|
+
m = path.match(/^\/api\/bots\/([\w-]+)\/projects\/order$/);
|
|
11529
|
+
if (m && method === "PATCH") {
|
|
11530
|
+
const bot = store.bot(m[1]);
|
|
11531
|
+
if (!bot)
|
|
11532
|
+
return json(res, 404, { error: "no such bot" });
|
|
11533
|
+
const body = await readBody(req);
|
|
11534
|
+
if (!body || typeof body !== "object" || Array.isArray(body) ||
|
|
11535
|
+
Object.keys(body).some((key) => key !== "projectIds") ||
|
|
11536
|
+
!Array.isArray(body.projectIds) || body.projectIds.some((id) => typeof id !== "string")) {
|
|
11537
|
+
return json(res, 400, { error: "projectIds must be an array of folder IDs" });
|
|
11538
|
+
}
|
|
11539
|
+
const projects = store.reorderProjects(bot.id, body.projectIds);
|
|
11540
|
+
if (!projects)
|
|
11541
|
+
return json(res, 400, { error: "projectIds must include each of this bot's folders exactly once" });
|
|
11542
|
+
return json(res, 200, { projects, bot: botWithThread(bot) });
|
|
11543
|
+
}
|
|
11440
11544
|
m = path.match(/^\/api\/bots\/([\w-]+)\/projects(?:\/([\w-]+))?$/);
|
|
11441
11545
|
if (m && ((method === "POST" && !m[2]) || (method === "PATCH" && m[2]) || (method === "DELETE" && m[2]))) {
|
|
11442
11546
|
const bot = store.bot(m[1]);
|
|
@@ -11451,18 +11555,23 @@ const handleRequest = async (req, res) => {
|
|
|
11451
11555
|
const body = await readBody(req);
|
|
11452
11556
|
if (!body || typeof body !== "object" || Array.isArray(body))
|
|
11453
11557
|
return json(res, 400, { error: "body must be a JSON object" });
|
|
11454
|
-
if (Object.keys(body).some((key) => key !== "name")) {
|
|
11558
|
+
if (Object.keys(body).some((key) => key !== "name" && key !== "emoji")) {
|
|
11455
11559
|
return json(res, 400, { error: "unsupported folder setting" });
|
|
11456
11560
|
}
|
|
11457
11561
|
if ((method === "POST" || body.name !== undefined) &&
|
|
11458
11562
|
(typeof body.name !== "string" || !body.name.trim() || body.name.trim().length > 80)) {
|
|
11459
11563
|
return json(res, 400, { error: "folder name must be between 1 and 80 characters" });
|
|
11460
11564
|
}
|
|
11565
|
+
if (body.emoji !== undefined && body.emoji !== null && !isProjectEmoji(body.emoji)) {
|
|
11566
|
+
return json(res, 400, { error: "folder emoji must be one emoji, or null to reset it" });
|
|
11567
|
+
}
|
|
11461
11568
|
const patch = {};
|
|
11462
11569
|
if (body.name !== undefined)
|
|
11463
11570
|
patch.name = body.name.trim();
|
|
11571
|
+
if (body.emoji !== undefined)
|
|
11572
|
+
patch.emoji = body.emoji;
|
|
11464
11573
|
const project = method === "POST"
|
|
11465
|
-
? store.createProject(bot.id, patch.name)
|
|
11574
|
+
? store.createProject(bot.id, patch.name, patch.emoji)
|
|
11466
11575
|
: store.patchProject(bot.id, m[2], patch);
|
|
11467
11576
|
return json(res, method === "POST" ? 201 : 200, { project, bot: botWithThread(bot) });
|
|
11468
11577
|
}
|
|
@@ -12567,6 +12676,7 @@ const handleRequest = async (req, res) => {
|
|
|
12567
12676
|
key !== "imageGen" &&
|
|
12568
12677
|
key !== "vps" &&
|
|
12569
12678
|
key !== "rooms" &&
|
|
12679
|
+
key !== "threads" &&
|
|
12570
12680
|
key !== "localVm" &&
|
|
12571
12681
|
key !== "features" &&
|
|
12572
12682
|
key !== "browserProfiles");
|
|
@@ -12604,6 +12714,12 @@ const handleRequest = async (req, res) => {
|
|
|
12604
12714
|
}
|
|
12605
12715
|
const status = configStatus();
|
|
12606
12716
|
broadcast({ kind: "config", ...status });
|
|
12717
|
+
if (patch.threads !== undefined) {
|
|
12718
|
+
drainQueuedSends();
|
|
12719
|
+
drainDelegationWakes();
|
|
12720
|
+
drainConnectorResumes();
|
|
12721
|
+
drainSecretResumes();
|
|
12722
|
+
}
|
|
12607
12723
|
if (mandatoryError)
|
|
12608
12724
|
throw mandatoryError;
|
|
12609
12725
|
return status;
|
|
@@ -13048,6 +13164,24 @@ catch (error) {
|
|
|
13048
13164
|
}
|
|
13049
13165
|
server.listen(PORT, "127.0.0.1", () => {
|
|
13050
13166
|
console.log(`openmausbot server on http://127.0.0.1:${PORT}`);
|
|
13167
|
+
// Startup work uses the same turn dispatcher and local tool endpoint as
|
|
13168
|
+
// ordinary chat. Start only once every registry is initialized and the
|
|
13169
|
+
// endpoint is listening; earlier dispatch can hit uninitialized bindings.
|
|
13170
|
+
routines.start();
|
|
13171
|
+
const leftover = pendingThreads();
|
|
13172
|
+
if (leftover.length)
|
|
13173
|
+
console.log(`delegations: ${leftover.length} thread(s) with queued handoffs from a previous run — draining`);
|
|
13174
|
+
for (const threadId of leftover) {
|
|
13175
|
+
const run = routines.runForThread(threadId);
|
|
13176
|
+
// A person can reuse a completed run's task for unrelated work. Only
|
|
13177
|
+
// discard the old run's handoffs, not a later user's persisted queue.
|
|
13178
|
+
const reused = run?.finishedAt !== undefined && store.botByThread(threadId) &&
|
|
13179
|
+
store.activePath(threadId).some((message) => message.role === "user" && message.at > run.finishedAt);
|
|
13180
|
+
if (run && !["running", "waiting"].includes(run.status) && !reused)
|
|
13181
|
+
discardDelegations(commsBus, threadId);
|
|
13182
|
+
else
|
|
13183
|
+
drainThreadDelegations(threadId);
|
|
13184
|
+
}
|
|
13051
13185
|
});
|
|
13052
13186
|
// A second listener for `openmausbot serve --tunnel` (server/tunnel.ts): the
|
|
13053
13187
|
// connector gateway on this machine forwards public traffic to this IPC path.
|