openmausbot 0.1.67 → 0.1.69
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-BwpNqgCm.js → index-BmHupgVJ.js} +1 -1
- package/dist/assets/index-COp5lMnv.js +279 -0
- package/dist/assets/index-Jk7TIeyW.css +1 -0
- package/dist/index.html +2 -2
- package/dist-server/container-mcp.js +40 -2
- package/dist-server/drivers/agents-proxy.js +112 -7
- package/dist-server/enterprise/server/index.js +14587 -0
- package/dist-server/index.js +1732 -681
- package/dist-server/openmausbot.js +939 -381
- package/dist-server/pair-cli.js +939 -381
- package/dist-server/server/avatar-image.js +86 -34
- package/dist-server/server/bot-package.js +60 -14
- package/dist-server/server/brand.js +7 -0
- package/dist-server/server/cli.js +23 -1
- package/dist-server/server/config.js +30 -3
- package/dist-server/server/delegations.js +56 -11
- package/dist-server/server/drivers/acp/qwen.js +142 -5
- package/dist-server/server/drivers/agents-proxy.js +135 -8
- package/dist-server/server/drivers/codex-device-auth.js +102 -3
- package/dist-server/server/drivers/codex-identity.js +115 -0
- package/dist-server/server/drivers/codex.js +6 -0
- package/dist-server/server/harness/registry.js +2 -0
- package/dist-server/server/index.js +220 -65
- package/dist-server/server/package-export.js +7 -0
- package/dist-server/server/provider-auth-sessions.js +24 -0
- package/dist-server/server/routine-requests.js +270 -25
- package/dist-server/server/routines.js +215 -21
- 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 +30 -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/shared/image-generation.js +28 -0
- package/dist-server/vps-container-mcp.js +40 -2
- 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
|
@@ -21,7 +21,7 @@ import { appendDecision, readDecisions } from "./decision-log.js";
|
|
|
21
21
|
import { validateBotCwd } from "./bot-cwd.js";
|
|
22
22
|
import { ATTACHMENTS_DIR, attachmentExists, cleanupStaleAttachmentPartials, deleteAttachment, extensionForMime, FILE_MAX_BYTES, IMAGE_MAX_BYTES, readAttachment, saveFile, saveImage, saveImageUpload, validateAttachmentUploadId, } from "./attachments.js";
|
|
23
23
|
import { messageFileDisposition, messageFileDownloadName, messageAttachmentName, messageFileRoots, messageImageTargetAt, messageReferencesFile, openMessageFile, } from "./message-file.js";
|
|
24
|
-
import { avatarGenerationRequestSchema, avatarGenerationStateMatches, generateAvatarImage, snapshotAvatarGenerationState, } from "./avatar-image.js";
|
|
24
|
+
import { avatarGenerationRequestSchema, avatarGenerationStateMatches, generateAvatarImage, avatarImageStatus, snapshotAvatarGenerationState, } from "./avatar-image.js";
|
|
25
25
|
import { fitsOnOneLine, parseBotProfilePatch } from "./bot-profile.js";
|
|
26
26
|
import { groupTurnCwd } from "./room-cwd.js";
|
|
27
27
|
import { RoomTurnDeadline, RoomTurnStallRegistry, roomTurnTimeoutMessage } from "./room-turn-timeout.js";
|
|
@@ -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";
|
|
@@ -54,7 +54,7 @@ import { claimRecallCrossings, recallCrossingLabel } from "./recall-disclosure.j
|
|
|
54
54
|
const SESSION_READ_MAX_CHARS = 8_000;
|
|
55
55
|
import { promptWithReply, transcriptText } from "./replies.js";
|
|
56
56
|
import { _loadPending, buildDelegationFailurePrompt, buildDelegationRevivalPrompt, DelegationWakeBudget, discardDelegations, drainDelegations, findDelegationReceipt, pendingDelegationInfo, pendingDelegationSnapshot, pendingThreads, queueDelegation, recordDelegationReceipt, releaseDelegationsWaitingOn, summarizeDelegatedActivity } from "./delegations.js";
|
|
57
|
-
import { cancelSteeredMessage, drainSteeredMessages, queuedSteeredMessage, queueSteeredMessage, } from "./steer-queue.js";
|
|
57
|
+
import { cancelSteeredMessage, drainSteeredMessages, onSteeredQueueChange, queuedSteerSnapshot, queuedSteeredMessage, queueSteeredMessage, } from "./steer-queue.js";
|
|
58
58
|
import { cancelChannelMessage, drainChannelMessages, queuedChannelMessage, queueChannelMessage, } from "./channel-queue.js";
|
|
59
59
|
import { acceptedSendMatch, parseSendId, sendFingerprint, SendSequencer, } from "./send-idempotency.js";
|
|
60
60
|
import { EventBus } from "./harness/bus.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
|
}
|
|
@@ -1364,6 +1368,7 @@ const publicBot = (bot) => ({
|
|
|
1364
1368
|
activeLeafId: store.activeLeaf(bot.threadId),
|
|
1365
1369
|
tasks: store.tasks(bot.id).map(wireTask),
|
|
1366
1370
|
});
|
|
1371
|
+
const publicBotQueuedMessages = () => queuedSteerSnapshot((botId, threadId) => Boolean(store.taskByThread(botId, threadId)));
|
|
1367
1372
|
// busyBotId names only the speaker that currently owns the provider process.
|
|
1368
1373
|
// A room turn is wider: it also includes async setup and every responder still
|
|
1369
1374
|
// queued behind that speaker. Keep that operation visible for its whole
|
|
@@ -1494,6 +1499,10 @@ function finishGroupGoalRun(groupId, operation, status, detail) {
|
|
|
1494
1499
|
// A calendar-triggered team goal reuses its RoutineRun id for this card.
|
|
1495
1500
|
// Manual goals have unrelated ids, so the manager safely ignores them.
|
|
1496
1501
|
const routineRun = routines?.finishGoalRun(run.runId, status, safeDetail);
|
|
1502
|
+
if (routineRun?.status === "cancelled") {
|
|
1503
|
+
pendingDelegationWakes.delete(operation.threadId);
|
|
1504
|
+
discardDelegations(commsBus, operation.threadId);
|
|
1505
|
+
}
|
|
1497
1506
|
// Member-level turn completions are intentionally private/intermediate for
|
|
1498
1507
|
// a team goal, so the normal direct-routine notification path never fires.
|
|
1499
1508
|
// Notify once from the correlated terminal receipt instead.
|
|
@@ -1502,13 +1511,15 @@ function finishGroupGoalRun(groupId, operation, status, detail) {
|
|
|
1502
1511
|
if (routineRun?.status === "waiting") {
|
|
1503
1512
|
const coordinator = store.bot(routineRun.botId);
|
|
1504
1513
|
if (coordinator) {
|
|
1505
|
-
|
|
1514
|
+
const notificationBot = routineSourceOwner(routineRun)?.bot ?? coordinator;
|
|
1515
|
+
notify(buildNotification("question", notificationBot, routineSourceThread(routineRun) ?? routineRun.threadId ?? operation.threadId, safeDetail || `${routineRun.routineName} needs your input`, { avatarUrl: notificationBot.avatarUrl }));
|
|
1506
1516
|
}
|
|
1507
1517
|
}
|
|
1508
1518
|
if (routineRun?.status === "completed") {
|
|
1509
1519
|
const coordinator = store.bot(routineRun.botId);
|
|
1510
1520
|
if (coordinator) {
|
|
1511
|
-
|
|
1521
|
+
const notificationBot = routineSourceOwner(routineRun)?.bot ?? coordinator;
|
|
1522
|
+
notify(buildNotification("done", notificationBot, routineSourceThread(routineRun) ?? routineRun.threadId ?? operation.threadId, safeDetail || routineRun.routineName, { avatarUrl: notificationBot.avatarUrl }));
|
|
1512
1523
|
}
|
|
1513
1524
|
}
|
|
1514
1525
|
const group = store.group(groupId);
|
|
@@ -1740,6 +1751,7 @@ store.onChange((change) => {
|
|
|
1740
1751
|
break;
|
|
1741
1752
|
case "thread.deleted":
|
|
1742
1753
|
routines?.forgetRoutineRequestReceiptsForThread(change.threadId);
|
|
1754
|
+
broadcast({ kind: "bot.queued", queues: publicBotQueuedMessages() });
|
|
1743
1755
|
break;
|
|
1744
1756
|
case "bot": {
|
|
1745
1757
|
const bot = store.bot(change.botId);
|
|
@@ -1883,6 +1895,7 @@ function broadcast(payload) {
|
|
|
1883
1895
|
}
|
|
1884
1896
|
}
|
|
1885
1897
|
}
|
|
1898
|
+
onSteeredQueueChange(() => broadcast({ kind: "bot.queued", queues: publicBotQueuedMessages() }));
|
|
1886
1899
|
// ── server-side event folding (upstream's ingestion worker, miniature) ──
|
|
1887
1900
|
// The canonical stream is the source of truth; the persisted transcript
|
|
1888
1901
|
// and every client view are projections of it.
|
|
@@ -2033,9 +2046,17 @@ const watchdog = new TurnWatchdog({
|
|
|
2033
2046
|
revokeInternalCapabilitiesForThread(turn.threadId);
|
|
2034
2047
|
repeats.settle(turn.threadId);
|
|
2035
2048
|
const bot = botForThread(turn.botId, turn.threadId);
|
|
2036
|
-
const
|
|
2049
|
+
const routineRun = activeRoutineRunForThread(turn.threadId);
|
|
2050
|
+
const instance = routineRun?.runOn === "cloud"
|
|
2051
|
+
? registry.instances().find((candidate) => candidate.driverKind === "boxAgent")
|
|
2052
|
+
: bot ? registry.get(bot.modelSelection.instanceId) : null;
|
|
2037
2053
|
void instance?.adapter.interruptTurn(turn.threadId).catch(() => { });
|
|
2038
2054
|
const minutes = Math.round(TURN_STALL_MS / 60_000);
|
|
2055
|
+
if (routineRun?.target === "bot") {
|
|
2056
|
+
routines?.failThread(turn.threadId, `No activity for ${minutes} minutes — the routine was stopped`);
|
|
2057
|
+
pendingDelegationWakes.delete(turn.threadId);
|
|
2058
|
+
discardDelegations(commsBus, turn.threadId);
|
|
2059
|
+
}
|
|
2039
2060
|
store.appendMessage(turn.threadId, {
|
|
2040
2061
|
role: "bot",
|
|
2041
2062
|
kind: "activity",
|
|
@@ -2826,7 +2847,8 @@ bus.subscribe((event) => {
|
|
|
2826
2847
|
store.setTaskActivity(bot.id, event.threadId, "waiting-on-you");
|
|
2827
2848
|
else if (asker.busy)
|
|
2828
2849
|
store.setActivity(asker.id, "waiting-on-you");
|
|
2829
|
-
|
|
2850
|
+
const notificationBot = (routineRun && routineSourceOwner(routineRun)?.bot) || asker;
|
|
2851
|
+
notify(buildNotification(permission ? "approval" : "question", notificationBot, (routineRun && routineSourceThread(routineRun)) || event.threadId, event.summary));
|
|
2830
2852
|
};
|
|
2831
2853
|
if (reviewTask && reviewMode === "enforce") {
|
|
2832
2854
|
// Avoid buzzing the owner for a card the reviewer is about to answer.
|
|
@@ -2991,13 +3013,14 @@ bus.subscribe((event) => {
|
|
|
2991
3013
|
// A failed peer turn stays a chip too. The bot that delegated is woken
|
|
2992
3014
|
// with the failure and answers the person in its own thread — buzzing
|
|
2993
3015
|
// here as well would ring twice for one piece of news.
|
|
2994
|
-
if (routineRun
|
|
3016
|
+
if ((!routineRun || routineRun.status === "completed") && !internal) {
|
|
2995
3017
|
// the frame carries the bot's avatar so every desktop client can
|
|
2996
3018
|
// show the notification under that bot's own face
|
|
2997
3019
|
const completionDetail = routineRun
|
|
2998
3020
|
? reply || routineRun.output || routineRun.routineName
|
|
2999
3021
|
: reply;
|
|
3000
|
-
|
|
3022
|
+
const notificationBot = (routineRun && routineSourceOwner(routineRun)?.bot) || bot;
|
|
3023
|
+
notify(buildNotification("done", notificationBot, routineReportThread ?? event.threadId, completionDetail, { avatarUrl: notificationBot.avatarUrl }));
|
|
3001
3024
|
}
|
|
3002
3025
|
if (screenPollers.has(event.threadId)) {
|
|
3003
3026
|
if (resourceOwner)
|
|
@@ -3064,9 +3087,22 @@ const delegationWatch = new Map();
|
|
|
3064
3087
|
// the cardContinuation resume pattern used for connector/credential cards.
|
|
3065
3088
|
const delegationWakeBudget = new DelegationWakeBudget();
|
|
3066
3089
|
const pendingDelegationWakes = new Map();
|
|
3067
|
-
function
|
|
3090
|
+
function activeRoutineRunForThread(threadId) {
|
|
3091
|
+
const run = routines?.runForThread(threadId);
|
|
3092
|
+
return run && ["running", "waiting"].includes(run.status) ? run : null;
|
|
3093
|
+
}
|
|
3094
|
+
function routineDelegationCanResume(threadId, routineRunId) {
|
|
3095
|
+
return !routineRunId || activeRoutineRunForThread(threadId)?.id === routineRunId;
|
|
3096
|
+
}
|
|
3097
|
+
function dispatchDelegationWake(botId, threadId, targetName, failureReason, routineRunId, budgetAcquired = false) {
|
|
3068
3098
|
if (!store.taskByThread(botId, threadId))
|
|
3069
3099
|
return;
|
|
3100
|
+
if (!routineDelegationCanResume(threadId, routineRunId))
|
|
3101
|
+
return;
|
|
3102
|
+
if (!budgetAcquired && !delegationWakeBudget.tryAcquire(threadId)) {
|
|
3103
|
+
routines?.failThread(threadId, "Delegation follow-up limit reached; review the run before retrying");
|
|
3104
|
+
return;
|
|
3105
|
+
}
|
|
3070
3106
|
const prompt = failureReason
|
|
3071
3107
|
? buildDelegationFailurePrompt(targetName, failureReason)
|
|
3072
3108
|
: buildDelegationRevivalPrompt(targetName);
|
|
@@ -3081,8 +3117,9 @@ function dispatchDelegationWake(botId, threadId, targetName, failureReason) {
|
|
|
3081
3117
|
return;
|
|
3082
3118
|
const message = error instanceof Error ? error.message : String(error);
|
|
3083
3119
|
// Raced with a user turn claiming the bot — retry once it settles.
|
|
3084
|
-
|
|
3085
|
-
|
|
3120
|
+
// This is the same logical wake, so keep its original budget charge.
|
|
3121
|
+
if (isTurnAdmissionBlocked(error)) {
|
|
3122
|
+
pendingDelegationWakes.set(threadId, { botId, targetName, failureReason, routineRunId, budgetAcquired: true });
|
|
3086
3123
|
return;
|
|
3087
3124
|
}
|
|
3088
3125
|
store.appendMessage(threadId, {
|
|
@@ -3093,37 +3130,43 @@ function dispatchDelegationWake(botId, threadId, targetName, failureReason) {
|
|
|
3093
3130
|
ok: false,
|
|
3094
3131
|
},
|
|
3095
3132
|
});
|
|
3133
|
+
routines?.failThread(threadId, `Could not resume after delegation: ${message}`);
|
|
3096
3134
|
});
|
|
3097
3135
|
}
|
|
3098
|
-
function wakeDelegationSource(source, threadId, targetName, failureReason) {
|
|
3136
|
+
function wakeDelegationSource(source, threadId, targetName, failureReason, routineRunId) {
|
|
3099
3137
|
if (!store.taskByThread(source.id, threadId))
|
|
3100
3138
|
return;
|
|
3139
|
+
if (!routineDelegationCanResume(threadId, routineRunId))
|
|
3140
|
+
return;
|
|
3101
3141
|
// Busy? Hold the wake until the source settles, then drain it — the
|
|
3102
3142
|
// delegated reply is already in the thread, so nothing is lost, and the
|
|
3103
3143
|
// source processes it the moment it is free rather than only on a later
|
|
3104
3144
|
// user nudge.
|
|
3105
3145
|
if (threadBusy(source.id, threadId) || activeGroupTurnForBot(source.id)) {
|
|
3106
|
-
pendingDelegationWakes.set(threadId, { botId: source.id, targetName, failureReason });
|
|
3146
|
+
pendingDelegationWakes.set(threadId, { botId: source.id, targetName, failureReason, routineRunId });
|
|
3107
3147
|
return;
|
|
3108
3148
|
}
|
|
3109
|
-
|
|
3110
|
-
return;
|
|
3111
|
-
dispatchDelegationWake(source.id, threadId, targetName, failureReason);
|
|
3149
|
+
dispatchDelegationWake(source.id, threadId, targetName, failureReason, routineRunId);
|
|
3112
3150
|
}
|
|
3113
3151
|
function drainDelegationWakes() {
|
|
3114
3152
|
for (const [threadId, entry] of pendingDelegationWakes) {
|
|
3115
|
-
if (!store.taskByThread(entry.botId, threadId)) {
|
|
3153
|
+
if (!store.taskByThread(entry.botId, threadId) || !routineDelegationCanResume(threadId, entry.routineRunId)) {
|
|
3116
3154
|
pendingDelegationWakes.delete(threadId);
|
|
3117
3155
|
continue;
|
|
3118
3156
|
}
|
|
3119
3157
|
if (threadBusy(entry.botId, threadId) || activeGroupTurnForBot(entry.botId))
|
|
3120
3158
|
continue;
|
|
3121
3159
|
pendingDelegationWakes.delete(threadId);
|
|
3122
|
-
|
|
3123
|
-
continue;
|
|
3124
|
-
dispatchDelegationWake(entry.botId, threadId, entry.targetName, entry.failureReason);
|
|
3160
|
+
dispatchDelegationWake(entry.botId, threadId, entry.targetName, entry.failureReason, entry.routineRunId, entry.budgetAcquired);
|
|
3125
3161
|
}
|
|
3126
3162
|
}
|
|
3163
|
+
function wakeUndispatchedDelegation(receipt, routineRunId) {
|
|
3164
|
+
const source = store.botByThread(receipt.sourceThreadId);
|
|
3165
|
+
if (!source)
|
|
3166
|
+
return;
|
|
3167
|
+
markTaskContextExternallyUpdated(source, receipt.sourceThreadId);
|
|
3168
|
+
wakeDelegationSource(source, receipt.sourceThreadId, receipt.toBotName, receipt.result || "the handoff did not run", routineRunId);
|
|
3169
|
+
}
|
|
3127
3170
|
// Provider-native sessions only know about messages produced inside their
|
|
3128
3171
|
// own turns. A delegated result is appended later by the harness, so mark the
|
|
3129
3172
|
// source task with a persisted, impossible-to-resume owner. Its next turn
|
|
@@ -3255,11 +3298,11 @@ function finalizeDelegationWatch(threadId, ok, reply = "", failureName = "Delega
|
|
|
3255
3298
|
// silent" gap). Failures wake it too — the user must hear the task did
|
|
3256
3299
|
// not finish. Idle-checked and burst-capped so a busy source or a
|
|
3257
3300
|
// re-delegating loop cannot spin up runs.
|
|
3258
|
-
if (ok
|
|
3259
|
-
wakeDelegationSource(source, watched.sourceThreadId, targetName);
|
|
3301
|
+
if (ok) {
|
|
3302
|
+
wakeDelegationSource(source, watched.sourceThreadId, targetName, undefined, watched.routineRunId);
|
|
3260
3303
|
}
|
|
3261
3304
|
else if (!ok) {
|
|
3262
|
-
wakeDelegationSource(source, watched.sourceThreadId, targetName, failureName || "the delegated turn did not finish");
|
|
3305
|
+
wakeDelegationSource(source, watched.sourceThreadId, targetName, failureName || "the delegated turn did not finish", watched.routineRunId);
|
|
3263
3306
|
}
|
|
3264
3307
|
}
|
|
3265
3308
|
}
|
|
@@ -3329,6 +3372,7 @@ const runDelegatedTurn = (toBotId, text, commsDepth, sourceThreadId, channel, ta
|
|
|
3329
3372
|
taskId,
|
|
3330
3373
|
sourceThreadId,
|
|
3331
3374
|
sourceBotId,
|
|
3375
|
+
routineRunId: activeRoutineRunForThread(sourceThreadId)?.id,
|
|
3332
3376
|
startedAtMs: Date.now(),
|
|
3333
3377
|
});
|
|
3334
3378
|
}
|
|
@@ -3365,6 +3409,10 @@ const runDelegatedTurn = (toBotId, text, commsDepth, sourceThreadId, channel, ta
|
|
|
3365
3409
|
reportStartFailure(err);
|
|
3366
3410
|
});
|
|
3367
3411
|
};
|
|
3412
|
+
function drainThreadDelegations(threadId) {
|
|
3413
|
+
const routineRunId = activeRoutineRunForThread(threadId)?.id;
|
|
3414
|
+
drainDelegations(commsBus, approvalBus, threadId, runDelegatedTurn, (receipt) => wakeUndispatchedDelegation(receipt, routineRunId));
|
|
3415
|
+
}
|
|
3368
3416
|
// Most waiting handoffs retry from a target's turn.completed event. Some
|
|
3369
3417
|
// setup, cancellation, room, watchdog, and provider-reload paths release a
|
|
3370
3418
|
// bot without that event, so every explicit idle release calls this same
|
|
@@ -3377,10 +3425,13 @@ function retryDelegationsWaitingOn(botId) {
|
|
|
3377
3425
|
delegationRetryBots.add(botId);
|
|
3378
3426
|
queueMicrotask(() => {
|
|
3379
3427
|
delegationRetryBots.delete(botId);
|
|
3428
|
+
// Explicit idle releases (room/setup/reload/watchdog fallbacks) may not
|
|
3429
|
+
// publish turn.completed. They free a waiting source continuation too.
|
|
3430
|
+
drainDelegationWakes();
|
|
3380
3431
|
if (store.bot(botId)?.busy)
|
|
3381
3432
|
return;
|
|
3382
3433
|
for (const waitingThread of releaseDelegationsWaitingOn(botId)) {
|
|
3383
|
-
|
|
3434
|
+
drainThreadDelegations(waitingThread);
|
|
3384
3435
|
}
|
|
3385
3436
|
});
|
|
3386
3437
|
}
|
|
@@ -3395,7 +3446,7 @@ bus.subscribe((event) => {
|
|
|
3395
3446
|
if (!event.ok)
|
|
3396
3447
|
discardDelegations(commsBus, event.threadId);
|
|
3397
3448
|
else
|
|
3398
|
-
|
|
3449
|
+
drainThreadDelegations(event.threadId);
|
|
3399
3450
|
// A settling bot frees itself as a delegation TARGET too: handoffs that
|
|
3400
3451
|
// found it busy earlier were kept queued (bounded retries) on their own
|
|
3401
3452
|
// source threads, and this is the moment they get their retry.
|
|
@@ -3441,7 +3492,24 @@ function drainQueuedSends() {
|
|
|
3441
3492
|
}),
|
|
3442
3493
|
// Provider completion can precede its dispatch promise: keep the queue
|
|
3443
3494
|
// intact until that exact handshake releases its runtime-only claim.
|
|
3444
|
-
(botId, threadId) => threadBusy(botId, threadId) || Boolean(activeGroupTurnForBot(botId)));
|
|
3495
|
+
(botId, threadId) => threadBusy(botId, threadId) || botAtThreadCapacity(botId) || Boolean(activeGroupTurnForBot(botId)));
|
|
3496
|
+
}
|
|
3497
|
+
/** Keep a person's words off the transcript until a direct-thread slot is
|
|
3498
|
+
* available. Reuse the existing cancellable, idempotent composer queue. */
|
|
3499
|
+
async function startOrQueueDirectMessage(botId, threadId, text, replyTo, sendId) {
|
|
3500
|
+
const capacity = botAtThreadCapacity(botId);
|
|
3501
|
+
if (capacity || threadBusy(botId, threadId)) {
|
|
3502
|
+
const reason = capacity ? "capacity" : undefined;
|
|
3503
|
+
const queued = queueSteeredMessage(botId, threadId, text, {
|
|
3504
|
+
replyToId: replyTo?.id,
|
|
3505
|
+
sendId,
|
|
3506
|
+
reason,
|
|
3507
|
+
prompt: promptWithReply(text, replyTo, cfg.profile?.name?.trim() || "User"),
|
|
3508
|
+
});
|
|
3509
|
+
return { ok: true, queued: true, queueId: queued.id, threadId, reason };
|
|
3510
|
+
}
|
|
3511
|
+
const message = await startTurn(botId, text, { threadId, replyTo, sendId });
|
|
3512
|
+
return { ok: true, threadId, message };
|
|
3445
3513
|
}
|
|
3446
3514
|
const screenPollers = new Map();
|
|
3447
3515
|
/** The preview shares the box's single command endpoint with the agent's
|
|
@@ -3576,6 +3644,19 @@ async function startTurn(botId, text, opts) {
|
|
|
3576
3644
|
if (!profile)
|
|
3577
3645
|
throw Object.assign(new Error("no such bot"), { status: 404 });
|
|
3578
3646
|
const threadId = opts?.threadId ?? profile.threadId;
|
|
3647
|
+
const continuingRoutine = opts?.cardContinuation ? activeRoutineRunForThread(threadId) : null;
|
|
3648
|
+
if (continuingRoutine) {
|
|
3649
|
+
const onDispatchError = opts?.onDispatchError;
|
|
3650
|
+
opts = {
|
|
3651
|
+
...opts,
|
|
3652
|
+
runOn: continuingRoutine.runOn,
|
|
3653
|
+
automationSource: continuingRoutine.triggerSource ?? (continuingRoutine.manual ? "manual" : "schedule"),
|
|
3654
|
+
onDispatchError: (message) => {
|
|
3655
|
+
routines?.failThread(threadId, message);
|
|
3656
|
+
onDispatchError?.(message);
|
|
3657
|
+
},
|
|
3658
|
+
};
|
|
3659
|
+
}
|
|
3579
3660
|
const bot = store.projectBotForTask(botId, threadId);
|
|
3580
3661
|
if (!bot)
|
|
3581
3662
|
throw Object.assign(new Error("no such task"), { status: 404 });
|
|
@@ -3600,8 +3681,8 @@ async function startTurn(botId, text, opts) {
|
|
|
3600
3681
|
if (activeGroupTurnForBot(botId)) {
|
|
3601
3682
|
throw Object.assign(new Error("the bot is already working in a channel — wait for it to finish"), { status: 409, code: "thread_busy" });
|
|
3602
3683
|
}
|
|
3603
|
-
if (
|
|
3604
|
-
throw Object.assign(new Error(
|
|
3684
|
+
if (botAtThreadCapacity(botId)) {
|
|
3685
|
+
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
3686
|
}
|
|
3606
3687
|
// Retire anything a previous turn left behind before minting this turn's
|
|
3607
3688
|
// integrations. Completion and interrupt paths do the same; this is the
|
|
@@ -4169,6 +4250,7 @@ async function startTurn(botId, text, opts) {
|
|
|
4169
4250
|
{ id: "credential", label: "Credentials", text: credentialPrompt },
|
|
4170
4251
|
{ id: "recall", label: "Recall", text: recallPrompt },
|
|
4171
4252
|
{ id: "routine", label: "Routines", text: routinePrompt },
|
|
4253
|
+
{ id: "routine-execution", label: "Routine execution", text: opts?.automationSource === "schedule" || opts?.automationSource === "manual" ? ROUTINE_EXECUTION_PROMPT : "" },
|
|
4172
4254
|
{ id: "profile", label: "Profile changes", text: profilePrompt },
|
|
4173
4255
|
{ id: "learn", label: "Skill authoring", text: learnPrompt },
|
|
4174
4256
|
{ id: "section-context", label: "Section context", text: sectionContextSystemPrompt(bot.section) },
|
|
@@ -4309,8 +4391,14 @@ function routineSourceOwner(run) {
|
|
|
4309
4391
|
const bot = store.bot(run.botId);
|
|
4310
4392
|
if (!bot)
|
|
4311
4393
|
return null;
|
|
4312
|
-
|
|
4313
|
-
|
|
4394
|
+
// The source is stamped from the confirmed request, never calendar input.
|
|
4395
|
+
// A request for a teammate runs as that teammate but reports to the bot
|
|
4396
|
+
// whose conversation held the card. Recheck their section before sharing
|
|
4397
|
+
// a result, since either bot may have moved since confirmation.
|
|
4398
|
+
const sourceBot = store.botByThread(threadId);
|
|
4399
|
+
if (sourceBot && sectionKey(sourceBot.section) === sectionKey(bot.section)) {
|
|
4400
|
+
return { bot: sourceBot, group: undefined, threadId };
|
|
4401
|
+
}
|
|
4314
4402
|
const group = store.groupByThread(threadId);
|
|
4315
4403
|
return group?.memberIds.includes(bot.id) ? { bot, group, threadId } : null;
|
|
4316
4404
|
}
|
|
@@ -4450,8 +4538,16 @@ async function stopBotForEmergencyApprovalDowngrade(botId) {
|
|
|
4450
4538
|
}
|
|
4451
4539
|
await directStop;
|
|
4452
4540
|
}
|
|
4541
|
+
// Load queued handoffs before scheduler recovery can fail an interrupted
|
|
4542
|
+
// run. Its failure callback can then durably drop that work immediately;
|
|
4543
|
+
// nothing dispatches until the listener is ready below.
|
|
4544
|
+
const commsBus = { store, broadcast };
|
|
4545
|
+
_loadPending();
|
|
4453
4546
|
routines = new RoutineManager({
|
|
4454
4547
|
emit: broadcast,
|
|
4548
|
+
hasPendingDelegations: (threadId) => pendingThreads().includes(threadId) ||
|
|
4549
|
+
[...delegationWatch.values()].some((watch) => watch.sourceThreadId === threadId) ||
|
|
4550
|
+
pendingDelegationWakes.has(threadId),
|
|
4455
4551
|
botState: (botId) => {
|
|
4456
4552
|
const bot = store.bot(botId);
|
|
4457
4553
|
return !bot ? "missing" : bot.busy ? "busy" : "ready";
|
|
@@ -4488,6 +4584,8 @@ routines = new RoutineManager({
|
|
|
4488
4584
|
},
|
|
4489
4585
|
interruptTurn: async (botId, threadId, runOn) => {
|
|
4490
4586
|
const bot = botForThread(botId, threadId);
|
|
4587
|
+
pendingDelegationWakes.delete(threadId);
|
|
4588
|
+
discardDelegations(commsBus, threadId);
|
|
4491
4589
|
cancelDirectTurnDispatch(botId, threadId);
|
|
4492
4590
|
revokeInternalCapabilitiesForThread(threadId);
|
|
4493
4591
|
const instance = runOn === "cloud"
|
|
@@ -4505,11 +4603,16 @@ routines = new RoutineManager({
|
|
|
4505
4603
|
interruptGoal: interruptRoutineGroupGoal,
|
|
4506
4604
|
onRunChanged: syncRoutineRunToSource,
|
|
4507
4605
|
onRunFailed: (run) => {
|
|
4606
|
+
if (run.threadId) {
|
|
4607
|
+
pendingDelegationWakes.delete(run.threadId);
|
|
4608
|
+
discardDelegations(commsBus, run.threadId);
|
|
4609
|
+
}
|
|
4508
4610
|
const bot = store.bot(run.botId);
|
|
4509
4611
|
if (!bot)
|
|
4510
4612
|
return;
|
|
4511
4613
|
const detail = run.error ? `${run.routineName}: ${run.error}` : run.routineName;
|
|
4512
|
-
|
|
4614
|
+
const notificationBot = routineSourceOwner(run)?.bot ?? bot;
|
|
4615
|
+
notify(buildNotification("routine-failed", notificationBot, routineSourceThread(run) ?? run.threadId ?? bot.threadId, detail));
|
|
4513
4616
|
},
|
|
4514
4617
|
});
|
|
4515
4618
|
// The scheduler receipt and room transcript live in separate durable stores.
|
|
@@ -4551,7 +4654,6 @@ if (recoveryOwners.length > 0) {
|
|
|
4551
4654
|
: [];
|
|
4552
4655
|
})));
|
|
4553
4656
|
}
|
|
4554
|
-
routines.start();
|
|
4555
4657
|
// Chat tools can prepare routine changes, but the harness applies them only
|
|
4556
4658
|
// after the user confirms a durable card. Keeping this beside the scheduler
|
|
4557
4659
|
// makes the card resolvable after an app restart without involving the model.
|
|
@@ -4639,6 +4741,13 @@ const agentRoutine = (routine, latestRun) => {
|
|
|
4639
4741
|
type: "interval",
|
|
4640
4742
|
everyMinutes: routine.schedule.everyMinutes,
|
|
4641
4743
|
anchorAt: new Date(routine.schedule.anchorAt).toISOString(),
|
|
4744
|
+
...(routine.schedule.weekdays === undefined
|
|
4745
|
+
? {}
|
|
4746
|
+
: { weekdays: routine.schedule.weekdays.map((day) => ROUTINE_WEEKDAY_NAMES[day]) }),
|
|
4747
|
+
...(routine.schedule.window === undefined ? {} : { window: { ...routine.schedule.window } }),
|
|
4748
|
+
...(routine.schedule.endsAt === undefined
|
|
4749
|
+
? {}
|
|
4750
|
+
: { endsAt: new Date(routine.schedule.endsAt).toISOString() }),
|
|
4642
4751
|
}
|
|
4643
4752
|
: {
|
|
4644
4753
|
type: "weekly",
|
|
@@ -4806,10 +4915,6 @@ function serializeRoomContext(threadId, userName, textOverride, readerBotId) {
|
|
|
4806
4915
|
})
|
|
4807
4916
|
.join("\n");
|
|
4808
4917
|
}
|
|
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
4918
|
// What each room has already taken from its bots. Keyed by room because the
|
|
4814
4919
|
// loop post_to_room can start is a property of the room, not of any one
|
|
4815
4920
|
// caller — three bots posting twice each is the same runaway as one bot
|
|
@@ -4830,18 +4935,6 @@ const approvalBus = { store, broadcast, notify };
|
|
|
4830
4935
|
if (stale)
|
|
4831
4936
|
console.log(`peer approvals: dismissed ${stale} card(s) left by a previous run`);
|
|
4832
4937
|
}
|
|
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
4938
|
async function runGroupMemberTurn(groupId, threadId, botId, hop,
|
|
4846
4939
|
// bots that already spoke for this user message — "@Scout ask @Pixel"
|
|
4847
4940
|
// must not run Pixel twice (once chained, once as a direct responder)
|
|
@@ -6402,7 +6495,7 @@ function dispatchConnectorResume(entry) {
|
|
|
6402
6495
|
onDispatchError: (message) => markConnectorResumeFailed(entry.threadId, entry.resumeKey, message),
|
|
6403
6496
|
}).catch((error) => {
|
|
6404
6497
|
const message = error instanceof Error ? error.message : String(error);
|
|
6405
|
-
if (
|
|
6498
|
+
if (isTurnAdmissionBlocked(error))
|
|
6406
6499
|
pendingConnectorResumes.set(`${entry.threadId}:${entry.resumeKey}`, entry);
|
|
6407
6500
|
else
|
|
6408
6501
|
markConnectorResumeFailed(entry.threadId, entry.resumeKey, message);
|
|
@@ -6529,7 +6622,7 @@ function dispatchSecretResume(entry) {
|
|
|
6529
6622
|
onDispatchError: (message) => markSecretResumeFailed(entry.threadId, entry.messageId, message),
|
|
6530
6623
|
}).catch((error) => {
|
|
6531
6624
|
const message = error instanceof Error ? error.message : String(error);
|
|
6532
|
-
if (
|
|
6625
|
+
if (isTurnAdmissionBlocked(error)) {
|
|
6533
6626
|
pendingSecretResumes.set(`${entry.threadId}:${entry.messageId}`, entry);
|
|
6534
6627
|
}
|
|
6535
6628
|
else {
|
|
@@ -6707,6 +6800,7 @@ function cliProbeEnvironment() {
|
|
|
6707
6800
|
"OMB_COMPOSIO_BROKER_TOKEN",
|
|
6708
6801
|
"OMB_TTS_KEY",
|
|
6709
6802
|
"OMB_OPENAI_IMAGE_KEY",
|
|
6803
|
+
"OMB_CUSTOM_IMAGE_KEY",
|
|
6710
6804
|
"ANTHROPIC_API_KEY",
|
|
6711
6805
|
"OPENAI_API_KEY",
|
|
6712
6806
|
]) {
|
|
@@ -6819,12 +6913,13 @@ function configStatus() {
|
|
|
6819
6913
|
// the chosen voice is a setting, not a secret; the key is reported the
|
|
6820
6914
|
// same configured-or-not way as every other credential
|
|
6821
6915
|
tts: tts.describeVoice(cfg),
|
|
6822
|
-
imageGen:
|
|
6916
|
+
imageGen: avatarImageStatus(cfg),
|
|
6823
6917
|
// not a secret — the sidebar shows it
|
|
6824
6918
|
profile: { name: cfg.profile?.name ?? "", email: cfg.profile?.email ?? "" },
|
|
6825
6919
|
// not a secret — the settings picker shows it; "" = follow the system
|
|
6826
6920
|
language: cfg.language ?? "",
|
|
6827
6921
|
rooms: { turnTimeoutMinutes: roomTurnTimeoutMinutes(cfg) },
|
|
6922
|
+
threads: { maxConcurrentPerBot: maxConcurrentBotThreads(cfg) },
|
|
6828
6923
|
localVm: {
|
|
6829
6924
|
mode: localVmMode(cfg),
|
|
6830
6925
|
maxInstances: localVmMaxInstances(cfg),
|
|
@@ -7186,6 +7281,11 @@ const handleRequest = async (req, res) => {
|
|
|
7186
7281
|
if (method === "GET" && path === "/api/health" && !gate.auth) {
|
|
7187
7282
|
return json(res, 200, { app: "openmausbot" });
|
|
7188
7283
|
}
|
|
7284
|
+
// The brand is public too: the sign-in page must carry the deployment's
|
|
7285
|
+
// name and icon before anyone has a session, and it holds nothing secret.
|
|
7286
|
+
if (method === "GET" && path === "/api/brand" && !gate.auth) {
|
|
7287
|
+
return json(res, 200, loadBrand());
|
|
7288
|
+
}
|
|
7189
7289
|
if (!gate.auth)
|
|
7190
7290
|
return json(res, gate.status, { error: gate.error });
|
|
7191
7291
|
const auth = gate.auth;
|
|
@@ -7872,6 +7972,7 @@ const handleRequest = async (req, res) => {
|
|
|
7872
7972
|
taskId,
|
|
7873
7973
|
sourceThreadId: fromThreadId,
|
|
7874
7974
|
sourceBotId: currentFrom.id,
|
|
7975
|
+
routineRunId: activeRoutineRunForThread(fromThreadId)?.id,
|
|
7875
7976
|
});
|
|
7876
7977
|
store.appendMessage(fromThreadId, {
|
|
7877
7978
|
role: "bot",
|
|
@@ -8642,6 +8743,7 @@ const handleRequest = async (req, res) => {
|
|
|
8642
8743
|
return json(res, 400, { error: "messages must be a non-negative whole number" });
|
|
8643
8744
|
return json(res, 200, {
|
|
8644
8745
|
bots: store.bots.map((bot) => ({ ...publicBot(bot), ...messagePage(bot.threadId, limit) })),
|
|
8746
|
+
botQueuedMessages: publicBotQueuedMessages(),
|
|
8645
8747
|
groups: store.groups.map((g) => ({ ...publicGroupState(g), ...messagePage(g.threadId, limit) })),
|
|
8646
8748
|
computerControl: Object.fromEntries(store.bots.map((bot) => {
|
|
8647
8749
|
const snapshot = computerControl.snapshot(bot.id);
|
|
@@ -9934,7 +10036,7 @@ const handleRequest = async (req, res) => {
|
|
|
9934
10036
|
if (!parsed.success) {
|
|
9935
10037
|
return json(res, 400, { error: `prompt must be at most 400 characters` });
|
|
9936
10038
|
}
|
|
9937
|
-
const generated = await generateAvatarImage(cfg
|
|
10039
|
+
const generated = await generateAvatarImage(cfg, existing, parsed.data.prompt);
|
|
9938
10040
|
const current = store.bot(existing.id);
|
|
9939
10041
|
if (!current)
|
|
9940
10042
|
return json(res, 404, { error: "no such bot" });
|
|
@@ -11094,7 +11196,7 @@ const handleRequest = async (req, res) => {
|
|
|
11094
11196
|
if (queued.text !== text || queued.replyToId !== replyTo?.id) {
|
|
11095
11197
|
throw Object.assign(new Error("sendId already belongs to another message"), { status: 409 });
|
|
11096
11198
|
}
|
|
11097
|
-
return { ok: true, queued: true, queueId: queued.id, threadId };
|
|
11199
|
+
return { ok: true, queued: true, queueId: queued.id, threadId, reason: queued.reason };
|
|
11098
11200
|
}
|
|
11099
11201
|
}
|
|
11100
11202
|
const currentAtStart = store.projectBotForTask(bot.id, threadId);
|
|
@@ -11155,8 +11257,7 @@ const handleRequest = async (req, res) => {
|
|
|
11155
11257
|
return { ok: true, steered: true, threadId, message };
|
|
11156
11258
|
}
|
|
11157
11259
|
if (!current.busy) {
|
|
11158
|
-
|
|
11159
|
-
return { ok: true, threadId, message };
|
|
11260
|
+
return startOrQueueDirectMessage(bot.id, threadId, text, replyTo, sendId);
|
|
11160
11261
|
}
|
|
11161
11262
|
const queued = queueSteeredMessage(current.id, threadId, text, {
|
|
11162
11263
|
replyToId: replyTo?.id,
|
|
@@ -11165,8 +11266,7 @@ const handleRequest = async (req, res) => {
|
|
|
11165
11266
|
});
|
|
11166
11267
|
return { ok: true, queued: true, queueId: queued.id, threadId };
|
|
11167
11268
|
}
|
|
11168
|
-
|
|
11169
|
-
return { ok: true, threadId, message };
|
|
11269
|
+
return startOrQueueDirectMessage(bot.id, threadId, text, replyTo, sendId);
|
|
11170
11270
|
});
|
|
11171
11271
|
return json(res, 202, receipt);
|
|
11172
11272
|
}
|
|
@@ -11437,6 +11537,22 @@ const handleRequest = async (req, res) => {
|
|
|
11437
11537
|
});
|
|
11438
11538
|
// Folders organize one bot's threads; they never own settings,
|
|
11439
11539
|
// transcripts or working directories. The project wire names stay stable.
|
|
11540
|
+
m = path.match(/^\/api\/bots\/([\w-]+)\/projects\/order$/);
|
|
11541
|
+
if (m && method === "PATCH") {
|
|
11542
|
+
const bot = store.bot(m[1]);
|
|
11543
|
+
if (!bot)
|
|
11544
|
+
return json(res, 404, { error: "no such bot" });
|
|
11545
|
+
const body = await readBody(req);
|
|
11546
|
+
if (!body || typeof body !== "object" || Array.isArray(body) ||
|
|
11547
|
+
Object.keys(body).some((key) => key !== "projectIds") ||
|
|
11548
|
+
!Array.isArray(body.projectIds) || body.projectIds.some((id) => typeof id !== "string")) {
|
|
11549
|
+
return json(res, 400, { error: "projectIds must be an array of folder IDs" });
|
|
11550
|
+
}
|
|
11551
|
+
const projects = store.reorderProjects(bot.id, body.projectIds);
|
|
11552
|
+
if (!projects)
|
|
11553
|
+
return json(res, 400, { error: "projectIds must include each of this bot's folders exactly once" });
|
|
11554
|
+
return json(res, 200, { projects, bot: botWithThread(bot) });
|
|
11555
|
+
}
|
|
11440
11556
|
m = path.match(/^\/api\/bots\/([\w-]+)\/projects(?:\/([\w-]+))?$/);
|
|
11441
11557
|
if (m && ((method === "POST" && !m[2]) || (method === "PATCH" && m[2]) || (method === "DELETE" && m[2]))) {
|
|
11442
11558
|
const bot = store.bot(m[1]);
|
|
@@ -11451,18 +11567,23 @@ const handleRequest = async (req, res) => {
|
|
|
11451
11567
|
const body = await readBody(req);
|
|
11452
11568
|
if (!body || typeof body !== "object" || Array.isArray(body))
|
|
11453
11569
|
return json(res, 400, { error: "body must be a JSON object" });
|
|
11454
|
-
if (Object.keys(body).some((key) => key !== "name")) {
|
|
11570
|
+
if (Object.keys(body).some((key) => key !== "name" && key !== "emoji")) {
|
|
11455
11571
|
return json(res, 400, { error: "unsupported folder setting" });
|
|
11456
11572
|
}
|
|
11457
11573
|
if ((method === "POST" || body.name !== undefined) &&
|
|
11458
11574
|
(typeof body.name !== "string" || !body.name.trim() || body.name.trim().length > 80)) {
|
|
11459
11575
|
return json(res, 400, { error: "folder name must be between 1 and 80 characters" });
|
|
11460
11576
|
}
|
|
11577
|
+
if (body.emoji !== undefined && body.emoji !== null && !isProjectEmoji(body.emoji)) {
|
|
11578
|
+
return json(res, 400, { error: "folder emoji must be one emoji, or null to reset it" });
|
|
11579
|
+
}
|
|
11461
11580
|
const patch = {};
|
|
11462
11581
|
if (body.name !== undefined)
|
|
11463
11582
|
patch.name = body.name.trim();
|
|
11583
|
+
if (body.emoji !== undefined)
|
|
11584
|
+
patch.emoji = body.emoji;
|
|
11464
11585
|
const project = method === "POST"
|
|
11465
|
-
? store.createProject(bot.id, patch.name)
|
|
11586
|
+
? store.createProject(bot.id, patch.name, patch.emoji)
|
|
11466
11587
|
: store.patchProject(bot.id, m[2], patch);
|
|
11467
11588
|
return json(res, method === "POST" ? 201 : 200, { project, bot: botWithThread(bot) });
|
|
11468
11589
|
}
|
|
@@ -11896,7 +12017,7 @@ const handleRequest = async (req, res) => {
|
|
|
11896
12017
|
const state = await providerAuthSessions.status(authStatus[1], auth.kind === "session" ? auth.session.id : "loopback", url.searchParams.get("flowId") ?? "");
|
|
11897
12018
|
return json(res, 200, { auth: state });
|
|
11898
12019
|
}
|
|
11899
|
-
const instanceAction = /^\/api\/instances\/([\w.-]+)\/(refresh-models|install|auth\/start|auth\/complete|auth\/cancel)$/.exec(path);
|
|
12020
|
+
const instanceAction = /^\/api\/instances\/([\w.-]+)\/(refresh-models|install|auth\/start|auth\/complete|auth\/cancel|auth\/sign-out)$/.exec(path);
|
|
11900
12021
|
if (method === "POST" && instanceAction) {
|
|
11901
12022
|
if (!String(req.headers["content-type"] ?? "").toLowerCase().startsWith("application/json")) {
|
|
11902
12023
|
return json(res, 415, { error: "content-type must be application/json" });
|
|
@@ -11929,6 +12050,13 @@ const handleRequest = async (req, res) => {
|
|
|
11929
12050
|
}
|
|
11930
12051
|
return json(res, 200, { auth: started });
|
|
11931
12052
|
}
|
|
12053
|
+
if (action === "auth/sign-out") {
|
|
12054
|
+
const instance = registry.get(instanceId);
|
|
12055
|
+
if (!instance)
|
|
12056
|
+
return json(res, 404, { error: "unknown instance" });
|
|
12057
|
+
await providerAuthSessions.signOut(instance, owner);
|
|
12058
|
+
return json(res, 200, { instances: await describeInstances() });
|
|
12059
|
+
}
|
|
11932
12060
|
if (action === "auth/complete") {
|
|
11933
12061
|
const body = await readBody(req);
|
|
11934
12062
|
const flowId = typeof body?.flowId === "string" ? body.flowId : "";
|
|
@@ -12504,6 +12632,8 @@ const handleRequest = async (req, res) => {
|
|
|
12504
12632
|
persisted.tts.key = "";
|
|
12505
12633
|
if (persisted.imageGen?.key !== undefined)
|
|
12506
12634
|
persisted.imageGen.key = "";
|
|
12635
|
+
if (persisted.imageGen?.customApiKey !== undefined)
|
|
12636
|
+
persisted.imageGen.customApiKey = "";
|
|
12507
12637
|
saveConfig(persisted);
|
|
12508
12638
|
configWriteCommitted = true;
|
|
12509
12639
|
syncCredentialEnv(patch);
|
|
@@ -12567,6 +12697,7 @@ const handleRequest = async (req, res) => {
|
|
|
12567
12697
|
key !== "imageGen" &&
|
|
12568
12698
|
key !== "vps" &&
|
|
12569
12699
|
key !== "rooms" &&
|
|
12700
|
+
key !== "threads" &&
|
|
12570
12701
|
key !== "localVm" &&
|
|
12571
12702
|
key !== "features" &&
|
|
12572
12703
|
key !== "browserProfiles");
|
|
@@ -12604,6 +12735,12 @@ const handleRequest = async (req, res) => {
|
|
|
12604
12735
|
}
|
|
12605
12736
|
const status = configStatus();
|
|
12606
12737
|
broadcast({ kind: "config", ...status });
|
|
12738
|
+
if (patch.threads !== undefined) {
|
|
12739
|
+
drainQueuedSends();
|
|
12740
|
+
drainDelegationWakes();
|
|
12741
|
+
drainConnectorResumes();
|
|
12742
|
+
drainSecretResumes();
|
|
12743
|
+
}
|
|
12607
12744
|
if (mandatoryError)
|
|
12608
12745
|
throw mandatoryError;
|
|
12609
12746
|
return status;
|
|
@@ -13048,6 +13185,24 @@ catch (error) {
|
|
|
13048
13185
|
}
|
|
13049
13186
|
server.listen(PORT, "127.0.0.1", () => {
|
|
13050
13187
|
console.log(`openmausbot server on http://127.0.0.1:${PORT}`);
|
|
13188
|
+
// Startup work uses the same turn dispatcher and local tool endpoint as
|
|
13189
|
+
// ordinary chat. Start only once every registry is initialized and the
|
|
13190
|
+
// endpoint is listening; earlier dispatch can hit uninitialized bindings.
|
|
13191
|
+
routines.start();
|
|
13192
|
+
const leftover = pendingThreads();
|
|
13193
|
+
if (leftover.length)
|
|
13194
|
+
console.log(`delegations: ${leftover.length} thread(s) with queued handoffs from a previous run — draining`);
|
|
13195
|
+
for (const threadId of leftover) {
|
|
13196
|
+
const run = routines.runForThread(threadId);
|
|
13197
|
+
// A person can reuse a completed run's task for unrelated work. Only
|
|
13198
|
+
// discard the old run's handoffs, not a later user's persisted queue.
|
|
13199
|
+
const reused = run?.finishedAt !== undefined && store.botByThread(threadId) &&
|
|
13200
|
+
store.activePath(threadId).some((message) => message.role === "user" && message.at > run.finishedAt);
|
|
13201
|
+
if (run && !["running", "waiting"].includes(run.status) && !reused)
|
|
13202
|
+
discardDelegations(commsBus, threadId);
|
|
13203
|
+
else
|
|
13204
|
+
drainThreadDelegations(threadId);
|
|
13205
|
+
}
|
|
13051
13206
|
});
|
|
13052
13207
|
// A second listener for `openmausbot serve --tunnel` (server/tunnel.ts): the
|
|
13053
13208
|
// connector gateway on this machine forwards public traffic to this IPC path.
|