openmausbot 0.1.68 → 0.1.70
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-B2F__IRS.css +1 -0
- package/dist/assets/index-CLj3t5Ba.js +280 -0
- package/dist/assets/{index-HqooKFTS.js → index-D1RGhLUK.js} +1 -1
- package/dist/index.html +2 -2
- package/dist-server/container-mcp.js +40 -2
- package/dist-server/drivers/agents-proxy.js +188 -5
- package/dist-server/index.js +2431 -984
- package/dist-server/local-computer-proxy.js +2 -0
- package/dist-server/openmausbot.js +923 -334
- package/dist-server/pair-cli.js +923 -334
- package/dist-server/prepare-cloudflared.js +20 -3
- package/dist-server/server/avatar-image.js +86 -34
- package/dist-server/server/bot-package.js +60 -14
- package/dist-server/server/config.js +23 -2
- package/dist-server/server/delegations.js +132 -74
- package/dist-server/server/drivers/acp/qwen.js +142 -5
- package/dist-server/server/drivers/agents-proxy.js +224 -4
- package/dist-server/server/drivers/claude-login-auth.js +102 -9
- package/dist-server/server/drivers/claude.js +11 -5
- 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/engine-install.js +137 -0
- package/dist-server/server/env-path.js +12 -0
- package/dist-server/server/harness/registry.js +33 -6
- package/dist-server/server/index.js +365 -26
- package/dist-server/server/package-export.js +7 -0
- package/dist-server/server/peer-provenance.js +6 -2
- 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 +297 -69
- package/dist-server/server/steer-queue.js +48 -1
- package/dist-server/server/store.js +22 -4
- package/dist-server/server/system-prompt.js +1 -0
- package/dist-server/server/team-backup.js +18 -4
- package/dist-server/shared/image-generation.js +28 -0
- package/dist-server/shared/team-backup.js +1 -0
- package/dist-server/vps-container-mcp.js +40 -2
- package/package.json +1 -1
- package/dist/assets/index-CbaejjQA.js +0 -278
- package/dist/assets/index-aJ5EWZOQ.css +0 -1
|
@@ -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";
|
|
@@ -37,6 +37,7 @@ import { ensureDirs, instanceConfigs, loadConfig, localVmMaxInstances, localVmMo
|
|
|
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";
|
|
40
|
+
import { registerEnginesBinDir } from "./engine-install.js";
|
|
40
41
|
import { describeSpawnFailure, execCli } from "./procs.js";
|
|
41
42
|
import { blockedTarget, buildNotification } from "./notify.js";
|
|
42
43
|
import { isEffortLevel, newId, } from "./contracts.js";
|
|
@@ -54,7 +55,7 @@ import { claimRecallCrossings, recallCrossingLabel } from "./recall-disclosure.j
|
|
|
54
55
|
const SESSION_READ_MAX_CHARS = 8_000;
|
|
55
56
|
import { promptWithReply, transcriptText } from "./replies.js";
|
|
56
57
|
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";
|
|
58
|
+
import { cancelSteeredMessage, drainSteeredMessages, onSteeredQueueChange, queuedSteerSnapshot, queuedSteeredMessage, queuedThreadPosition, queueSteeredMessage, } from "./steer-queue.js";
|
|
58
59
|
import { cancelChannelMessage, drainChannelMessages, queuedChannelMessage, queueChannelMessage, } from "./channel-queue.js";
|
|
59
60
|
import { acceptedSendMatch, parseSendId, sendFingerprint, SendSequencer, } from "./send-idempotency.js";
|
|
60
61
|
import { EventBus } from "./harness/bus.js";
|
|
@@ -78,7 +79,7 @@ import { fetchSkillFromSource } from "./skill-fetch.js";
|
|
|
78
79
|
import { expandLearnTurnText, learnSource } from "./skill-learn.js";
|
|
79
80
|
import { expandSetupTurnText, setupModeActive, setupSystemPrompt } from "./setup-mode.js";
|
|
80
81
|
import { checkSoulDrift, readSoulDrift, soulFile, writeSoulMirror } from "./bot-folder.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
|
+
import { buildSystemPrompt, computerPrompt, mentionPrompt, COMPOSIO_PROMPT, CREDENTIAL_PROMPT, THREADS_PROMPT, LEARN_PROMPT, PROFILE_PROMPT, ROUTINE_PROMPT, ROUTINE_EXECUTION_PROMPT, WEBHOOK_PROMPT, } from "./system-prompt.js";
|
|
82
83
|
import { readCuaConnection, gatedLocalComputer } from "./local-computer.js";
|
|
83
84
|
import { discoverExistingPerBotLocalVms, localVmInventoryEntry, shouldArmLocalVmIdle, } from "./local-vm-inventory.js";
|
|
84
85
|
import { LocalVmIdleTimer } from "./local-vm-idle.js";
|
|
@@ -208,6 +209,9 @@ function customDomainStatus() {
|
|
|
208
209
|
};
|
|
209
210
|
}
|
|
210
211
|
const registry = new ProviderRegistry(BUILT_IN_DRIVERS);
|
|
212
|
+
// Engines installed from Settings live under the data directory and win over
|
|
213
|
+
// any other copy on PATH.
|
|
214
|
+
registerEnginesBinDir();
|
|
211
215
|
const providerAuthSessions = new ProviderAuthSessions();
|
|
212
216
|
await registry.load(instanceConfigs(cfg));
|
|
213
217
|
const bundledSkills = loadBundledSkills();
|
|
@@ -426,6 +430,7 @@ function agentsIntegration(botId, threadId, depth, skillAuthoring, generation) {
|
|
|
426
430
|
kind: "agents",
|
|
427
431
|
skillAuthoring,
|
|
428
432
|
createdBots: 0,
|
|
433
|
+
openedThreads: 0,
|
|
429
434
|
});
|
|
430
435
|
return {
|
|
431
436
|
command: process.execPath,
|
|
@@ -672,7 +677,7 @@ async function browserIntegration(botId, profile, turn) {
|
|
|
672
677
|
if (!turn)
|
|
673
678
|
return { profile: partitionId, session, spec, integration: spec };
|
|
674
679
|
const token = mintInternalCapability({ botId, ...turn, browserSession: session,
|
|
675
|
-
kind: "browser", depth: 0, skillAuthoring: false, createdBots: 0 });
|
|
680
|
+
kind: "browser", depth: 0, skillAuthoring: false, createdBots: 0, openedThreads: 0 });
|
|
676
681
|
return { profile: partitionId, session, spec, integration: {
|
|
677
682
|
command: process.execPath, args: [SPAWNED_PROXIES.browser], env: {
|
|
678
683
|
...AGENTS_NODE_FLAG, OMB_BROWSER_TOKEN: token, OMB_HARNESS_URL: `http://127.0.0.1:${PORT}`,
|
|
@@ -708,6 +713,7 @@ function connectedAppsIntegration(botId, threadId, generation) {
|
|
|
708
713
|
kind: "connectors",
|
|
709
714
|
skillAuthoring: false,
|
|
710
715
|
createdBots: 0,
|
|
716
|
+
openedThreads: 0,
|
|
711
717
|
});
|
|
712
718
|
return composio.mcpIntegration(cfg, {
|
|
713
719
|
harnessUrl: `http://127.0.0.1:${PORT}`,
|
|
@@ -760,6 +766,7 @@ function controlIntegration(botId, threadId, generation, localVmTarget) {
|
|
|
760
766
|
...(localVmTarget ? { localVmTarget } : {}),
|
|
761
767
|
skillAuthoring: false,
|
|
762
768
|
createdBots: 0,
|
|
769
|
+
openedThreads: 0,
|
|
763
770
|
}),
|
|
764
771
|
};
|
|
765
772
|
}
|
|
@@ -1368,6 +1375,7 @@ const publicBot = (bot) => ({
|
|
|
1368
1375
|
activeLeafId: store.activeLeaf(bot.threadId),
|
|
1369
1376
|
tasks: store.tasks(bot.id).map(wireTask),
|
|
1370
1377
|
});
|
|
1378
|
+
const publicBotQueuedMessages = () => queuedSteerSnapshot((botId, threadId) => Boolean(store.taskByThread(botId, threadId)));
|
|
1371
1379
|
// busyBotId names only the speaker that currently owns the provider process.
|
|
1372
1380
|
// A room turn is wider: it also includes async setup and every responder still
|
|
1373
1381
|
// queued behind that speaker. Keep that operation visible for its whole
|
|
@@ -1750,6 +1758,14 @@ store.onChange((change) => {
|
|
|
1750
1758
|
break;
|
|
1751
1759
|
case "thread.deleted":
|
|
1752
1760
|
routines?.forgetRoutineRequestReceiptsForThread(change.threadId);
|
|
1761
|
+
// A deleted destination must not strand an approval in an internal
|
|
1762
|
+
// task. Keep each run's snapshot and expose its execution as fallback.
|
|
1763
|
+
for (const run of routines?.listRuns() ?? []) {
|
|
1764
|
+
if (run.resultsThreadId === change.threadId || run.sourceThreadId === change.threadId) {
|
|
1765
|
+
syncRoutineRunToSource(run);
|
|
1766
|
+
}
|
|
1767
|
+
}
|
|
1768
|
+
broadcast({ kind: "bot.queued", queues: publicBotQueuedMessages() });
|
|
1753
1769
|
break;
|
|
1754
1770
|
case "bot": {
|
|
1755
1771
|
const bot = store.bot(change.botId);
|
|
@@ -1893,6 +1909,7 @@ function broadcast(payload) {
|
|
|
1893
1909
|
}
|
|
1894
1910
|
}
|
|
1895
1911
|
}
|
|
1912
|
+
onSteeredQueueChange(() => broadcast({ kind: "bot.queued", queues: publicBotQueuedMessages() }));
|
|
1896
1913
|
// ── server-side event folding (upstream's ingestion worker, miniature) ──
|
|
1897
1914
|
// The canonical stream is the source of truth; the persisted transcript
|
|
1898
1915
|
// and every client view are projections of it.
|
|
@@ -2999,13 +3016,12 @@ bus.subscribe((event) => {
|
|
|
2999
3016
|
costUsd: event.cost ?? null,
|
|
3000
3017
|
});
|
|
3001
3018
|
const routineReportThread = routineRun ? routineSourceThread(routineRun) : null;
|
|
3002
|
-
|
|
3003
|
-
//
|
|
3004
|
-
// hidden execution task should not light up the bot's 1:1 sidebar too.
|
|
3019
|
+
// A routine's result belongs to its reporting thread's unread state.
|
|
3020
|
+
// Its internal execution should not light up the sidebar as well.
|
|
3005
3021
|
// Neither should a peer's hop: the exchange is already recorded in the
|
|
3006
3022
|
// pair channel and chipped into both threads, which is the whole of
|
|
3007
3023
|
// what the person needs to be able to find it.
|
|
3008
|
-
if (!
|
|
3024
|
+
if (!routineReportThread && !internal)
|
|
3009
3025
|
store.patchTask(bot.id, event.threadId, { unread: true });
|
|
3010
3026
|
// A failed peer turn stays a chip too. The bot that delegated is woken
|
|
3011
3027
|
// with the failure and answers the person in its own thread — buzzing
|
|
@@ -3354,13 +3370,27 @@ bus.subscribe((event) => {
|
|
|
3354
3370
|
/** How a drained delegation becomes a real turn on the target. Shared by
|
|
3355
3371
|
* the settle-time drain and the boot-time drain of what a previous process
|
|
3356
3372
|
* left queued. */
|
|
3357
|
-
const runDelegatedTurn = (toBotId,
|
|
3373
|
+
const runDelegatedTurn = (toBotId, rawText, commsDepth, sourceThreadId, channel, taskId, sourceBotId, openedThreadId) => {
|
|
3358
3374
|
// startTurn REJECTS on an ordinary condition — busy target, deleted bot,
|
|
3359
3375
|
// unavailable provider. Unhandled, that rejection is fatal to the
|
|
3360
3376
|
// harness (Node's default), which in the packaged app kills the server
|
|
3361
3377
|
// child. Every delegation failure has to land as a chip instead.
|
|
3362
|
-
|
|
3378
|
+
// A fresh-thread handoff runs in the thread the opener created — the
|
|
3379
|
+
// drain already dropped it if that thread is gone — never in whatever
|
|
3380
|
+
// the person is looking at.
|
|
3381
|
+
const targetThreadId = openedThreadId ?? store.bot(toBotId)?.threadId;
|
|
3363
3382
|
const target = store.bot(toBotId);
|
|
3383
|
+
const opener = store.bot(sourceBotId);
|
|
3384
|
+
const unattended = isUnattended(sourceBotId, sourceThreadId);
|
|
3385
|
+
// The first line of an opened thread is another bot's words: it carries
|
|
3386
|
+
// the same provenance note every relayed peer line does, structurally
|
|
3387
|
+
// (peerAsk) as well as in the text.
|
|
3388
|
+
const peerAsk = openedThreadId && opener
|
|
3389
|
+
? { botId: opener.id, name: opener.name, unattended: unattended || undefined }
|
|
3390
|
+
: undefined;
|
|
3391
|
+
const text = openedThreadId && opener
|
|
3392
|
+
? withPeerProvenance(rawText, { botName: opener.name, delivery: "start_thread", unattended })
|
|
3393
|
+
: rawText;
|
|
3364
3394
|
if (targetThreadId) {
|
|
3365
3395
|
delegationWatch.set(targetThreadId, {
|
|
3366
3396
|
channelId: channel?.id,
|
|
@@ -3397,7 +3427,8 @@ const runDelegatedTurn = (toBotId, text, commsDepth, sourceThreadId, channel, ta
|
|
|
3397
3427
|
return startTurn(toBotId, text, {
|
|
3398
3428
|
threadId: targetThreadId,
|
|
3399
3429
|
commsDepth,
|
|
3400
|
-
unattended
|
|
3430
|
+
unattended,
|
|
3431
|
+
peerAsk,
|
|
3401
3432
|
// startTurn schedules provider/integration setup after marking the bot
|
|
3402
3433
|
// busy. Those asynchronous setup failures do not emit turn.completed,
|
|
3403
3434
|
// so clear the watch and report them through this callback too.
|
|
@@ -3425,9 +3456,16 @@ function retryDelegationsWaitingOn(botId) {
|
|
|
3425
3456
|
// Explicit idle releases (room/setup/reload/watchdog fallbacks) may not
|
|
3426
3457
|
// publish turn.completed. They free a waiting source continuation too.
|
|
3427
3458
|
drainDelegationWakes();
|
|
3428
|
-
|
|
3459
|
+
// A bot still busy in one thread has nevertheless freed a slot: the
|
|
3460
|
+
// fresh-thread handoffs waiting on it can move, while the active-thread
|
|
3461
|
+
// ones keep waiting for it to go idle, as they always have.
|
|
3462
|
+
const stillBusy = store.bot(botId)?.busy === true;
|
|
3463
|
+
if (stillBusy && botAtThreadCapacity(botId))
|
|
3429
3464
|
return;
|
|
3430
|
-
|
|
3465
|
+
const released = stillBusy
|
|
3466
|
+
? releaseDelegationsWaitingOn(botId, (item) => item.targetThreadId !== undefined)
|
|
3467
|
+
: releaseDelegationsWaitingOn(botId);
|
|
3468
|
+
for (const waitingThread of released) {
|
|
3431
3469
|
drainThreadDelegations(waitingThread);
|
|
3432
3470
|
}
|
|
3433
3471
|
});
|
|
@@ -3471,13 +3509,15 @@ bus.subscribe((event) => {
|
|
|
3471
3509
|
drainDelegationWakes();
|
|
3472
3510
|
});
|
|
3473
3511
|
function drainQueuedSends() {
|
|
3474
|
-
drainSteeredMessages(store, (botId, threadId, prompt, userMessage, excludeIds) =>
|
|
3475
|
-
// A plain attended turn — no automationSource, no
|
|
3476
|
-
//
|
|
3512
|
+
drainSteeredMessages(store, (botId, threadId, prompt, userMessage, excludeIds, unattended) =>
|
|
3513
|
+
// A plain attended turn — no automationSource, no comms depth: exactly
|
|
3514
|
+
// what typing the same words into an idle bot would run. The one
|
|
3515
|
+
// exception is `unattended`: a thread a bot opened on itself while
|
|
3516
|
+
// nobody was watching stays unwatched through the wait for a slot.
|
|
3477
3517
|
// Drain just appended the held lines; userMessage keeps startTurn
|
|
3478
3518
|
// from duplicating the last one, and excludeIds drops every drained
|
|
3479
3519
|
// line from the transcript-replay so they are not also in `prompt`.
|
|
3480
|
-
startTurn(botId, prompt, { threadId, userMessage, excludeMessageIds: excludeIds }).then(() => undefined).catch((err) => {
|
|
3520
|
+
startTurn(botId, prompt, { threadId, userMessage, excludeMessageIds: excludeIds, unattended }).then(() => undefined).catch((err) => {
|
|
3481
3521
|
store.appendMessage(threadId, {
|
|
3482
3522
|
role: "bot",
|
|
3483
3523
|
kind: "activity",
|
|
@@ -3508,6 +3548,36 @@ async function startOrQueueDirectMessage(botId, threadId, text, replyTo, sendId)
|
|
|
3508
3548
|
const message = await startTurn(botId, text, { threadId, replyTo, sendId });
|
|
3509
3549
|
return { ok: true, threadId, message };
|
|
3510
3550
|
}
|
|
3551
|
+
/** How many start_thread calls one turn may make. Same spirit as the
|
|
3552
|
+
* create-bot ceiling above: a handful is a plan, more is a fan-out. */
|
|
3553
|
+
const MAX_THREADS_OPENED_PER_TURN = 5;
|
|
3554
|
+
/** A thread a bot opened on itself gets its first turn exactly the way a
|
|
3555
|
+
* person's message would: it runs now if the bot has a free slot, and
|
|
3556
|
+
* otherwise waits in the same composer queue, in line behind whatever the
|
|
3557
|
+
* bot already has waiting. The only inheritance is `unattended` — a bot
|
|
3558
|
+
* nobody is watching does not become watched by opening a thread. */
|
|
3559
|
+
async function startOrQueueOpenedThread(botId, threadId, text, unattended) {
|
|
3560
|
+
// A room turn holds the bot too (startTurn refuses a direct turn during
|
|
3561
|
+
// one); the drain's own block check already waits for it, so the words
|
|
3562
|
+
// queue here rather than bounce.
|
|
3563
|
+
if (botAtThreadCapacity(botId) || activeGroupTurnForBot(botId)) {
|
|
3564
|
+
queueSteeredMessage(botId, threadId, text, { reason: "capacity", unattended });
|
|
3565
|
+
return { state: "queued", position: queuedThreadPosition(botId, threadId) ?? 1 };
|
|
3566
|
+
}
|
|
3567
|
+
try {
|
|
3568
|
+
await startTurn(botId, text, { threadId, unattended });
|
|
3569
|
+
return { state: "running" };
|
|
3570
|
+
}
|
|
3571
|
+
catch (error) {
|
|
3572
|
+
const why = error instanceof Error ? error.message : String(error);
|
|
3573
|
+
store.appendMessage(threadId, {
|
|
3574
|
+
role: "bot",
|
|
3575
|
+
kind: "activity",
|
|
3576
|
+
tool: { name: `error: this thread could not start — ${why.slice(0, 120)}`, ok: false },
|
|
3577
|
+
});
|
|
3578
|
+
return { state: "failed", error: why };
|
|
3579
|
+
}
|
|
3580
|
+
}
|
|
3511
3581
|
const screenPollers = new Map();
|
|
3512
3582
|
/** The preview shares the box's single command endpoint with the agent's
|
|
3513
3583
|
* own actions, so every frame we take is latency stolen from the work the
|
|
@@ -3763,6 +3833,10 @@ async function startTurn(botId, text, opts) {
|
|
|
3763
3833
|
if (!opts?.cardContinuation) {
|
|
3764
3834
|
if (commsDepth === 0 && opts?.automationSource === undefined && !opts?.unattended) {
|
|
3765
3835
|
personAskAt.set(threadId, userMessage.at);
|
|
3836
|
+
// Continuing an old run as a normal conversation makes that task
|
|
3837
|
+
// visible again; these new replies are not routine report updates.
|
|
3838
|
+
if (task.routineRunId)
|
|
3839
|
+
store.patchTask(bot.id, threadId, { routineRunId: undefined });
|
|
3766
3840
|
}
|
|
3767
3841
|
else {
|
|
3768
3842
|
personAskAt.delete(threadId);
|
|
@@ -4164,7 +4238,7 @@ async function startTurn(botId, text, opts) {
|
|
|
4164
4238
|
// model mostly did not think to make.
|
|
4165
4239
|
? peerRosterSystemPrompt(sectionPeers)
|
|
4166
4240
|
: "";
|
|
4167
|
-
const credentialPrompt = integrations.agents ? CREDENTIAL_PROMPT : "";
|
|
4241
|
+
const credentialPrompt = integrations.agents ? CREDENTIAL_PROMPT + THREADS_PROMPT : "";
|
|
4168
4242
|
const routinePrompt = integrations.agents ? ROUTINE_PROMPT : "";
|
|
4169
4243
|
const profilePrompt = integrations.agents ? PROFILE_PROMPT : "";
|
|
4170
4244
|
const recallPrompt = integrations.agents ? SESSION_SEARCH_SYSTEM_PROMPT : "";
|
|
@@ -4380,6 +4454,13 @@ async function startTurn(botId, text, opts) {
|
|
|
4380
4454
|
// The scheduler owns timing and receipts; the existing harness remains the
|
|
4381
4455
|
// only owner of provider sessions, approvals, tools, computers and messages.
|
|
4382
4456
|
function routineSourceOwner(run) {
|
|
4457
|
+
if (run.resultsThreadId) {
|
|
4458
|
+
const bot = store.bot(run.botId);
|
|
4459
|
+
const task = bot && store.taskByThread(bot.id, run.resultsThreadId);
|
|
4460
|
+
return bot && !bot.hidden && task && !task.routineRunId
|
|
4461
|
+
? { bot, group: undefined, threadId: task.threadId }
|
|
4462
|
+
: null;
|
|
4463
|
+
}
|
|
4383
4464
|
const threadId = run.sourceThreadId?.trim();
|
|
4384
4465
|
if (!threadId)
|
|
4385
4466
|
return null;
|
|
@@ -4393,7 +4474,8 @@ function routineSourceOwner(run) {
|
|
|
4393
4474
|
// whose conversation held the card. Recheck their section before sharing
|
|
4394
4475
|
// a result, since either bot may have moved since confirmation.
|
|
4395
4476
|
const sourceBot = store.botByThread(threadId);
|
|
4396
|
-
if (sourceBot &&
|
|
4477
|
+
if (sourceBot && !sourceBot.hidden && !store.taskByThread(sourceBot.id, threadId)?.routineRunId &&
|
|
4478
|
+
sectionKey(sourceBot.section) === sectionKey(bot.section)) {
|
|
4397
4479
|
return { bot: sourceBot, group: undefined, threadId };
|
|
4398
4480
|
}
|
|
4399
4481
|
const group = store.groupByThread(threadId);
|
|
@@ -4410,6 +4492,7 @@ function routineRunCard(run) {
|
|
|
4410
4492
|
runId: run.id,
|
|
4411
4493
|
routineId: run.routineId,
|
|
4412
4494
|
routineName: redactSecretsInText(run.routineName),
|
|
4495
|
+
scheduledFor: run.scheduledFor,
|
|
4413
4496
|
status: run.status,
|
|
4414
4497
|
};
|
|
4415
4498
|
if (run.goalStatus)
|
|
@@ -4452,8 +4535,16 @@ function routineRunFallbackText(card) {
|
|
|
4452
4535
|
* another chat message. */
|
|
4453
4536
|
function syncRoutineRunToSource(run) {
|
|
4454
4537
|
const source = routineSourceOwner(run);
|
|
4455
|
-
if (!source)
|
|
4538
|
+
if (!source) {
|
|
4539
|
+
const execution = run.threadId ? store.taskByThread(run.botId, run.threadId) : null;
|
|
4540
|
+
if (execution?.routineRunId === run.id) {
|
|
4541
|
+
store.patchTask(run.botId, execution.threadId, {
|
|
4542
|
+
routineRunId: undefined,
|
|
4543
|
+
...(["waiting", "completed", "failed", "missed"].includes(run.status) ? { unread: true } : {}),
|
|
4544
|
+
});
|
|
4545
|
+
}
|
|
4456
4546
|
return null;
|
|
4547
|
+
}
|
|
4457
4548
|
const sourceThreadId = source.threadId;
|
|
4458
4549
|
const card = routineRunCard(run);
|
|
4459
4550
|
const text = routineRunFallbackText(card);
|
|
@@ -4474,6 +4565,18 @@ function syncRoutineRunToSource(run) {
|
|
|
4474
4565
|
}
|
|
4475
4566
|
store.appendMessage(sourceThreadId, message);
|
|
4476
4567
|
}
|
|
4568
|
+
// Mark only the fresh execution, after its first running card persisted
|
|
4569
|
+
// and before dispatch adds any transcript. A user can later promote it by
|
|
4570
|
+
// sending a normal follow-up; replaying/marking the old receipt seen must
|
|
4571
|
+
// never hide that conversation again.
|
|
4572
|
+
if (run.target === "bot" && run.triggerSource !== "webhook" && run.threadId && run.threadId !== sourceThreadId) {
|
|
4573
|
+
const execution = store.taskByThread(run.botId, run.threadId);
|
|
4574
|
+
const freshExecution = execution && run.status === "running" && !execution.routineRunId &&
|
|
4575
|
+
store.messagesFor(run.threadId).length === 0;
|
|
4576
|
+
if (execution && (freshExecution || (execution.routineRunId === run.id && execution.unread))) {
|
|
4577
|
+
store.patchTask(run.botId, run.threadId, { routineRunId: run.id, unread: false });
|
|
4578
|
+
}
|
|
4579
|
+
}
|
|
4477
4580
|
// Merely queueing/running is ambient progress. Attention and terminal
|
|
4478
4581
|
// states become unread in the conversation where the user asked for them.
|
|
4479
4582
|
if (statusChanged && ["waiting", "completed", "failed", "missed"].includes(run.status)) {
|
|
@@ -4538,7 +4641,7 @@ async function stopBotForEmergencyApprovalDowngrade(botId) {
|
|
|
4538
4641
|
// Load queued handoffs before scheduler recovery can fail an interrupted
|
|
4539
4642
|
// run. Its failure callback can then durably drop that work immediately;
|
|
4540
4643
|
// nothing dispatches until the listener is ready below.
|
|
4541
|
-
const commsBus = { store, broadcast };
|
|
4644
|
+
const commsBus = { store, broadcast, threadSlotFree: (botId) => !botAtThreadCapacity(botId) };
|
|
4542
4645
|
_loadPending();
|
|
4543
4646
|
routines = new RoutineManager({
|
|
4544
4647
|
emit: broadcast,
|
|
@@ -4570,6 +4673,25 @@ routines = new RoutineManager({
|
|
|
4570
4673
|
return task;
|
|
4571
4674
|
},
|
|
4572
4675
|
createGoalTask: (groupId, title) => store.createGroupTask(groupId, title, false),
|
|
4676
|
+
isResultsThread: (botId, threadId) => {
|
|
4677
|
+
const bot = store.bot(botId);
|
|
4678
|
+
const task = store.taskByThread(botId, threadId);
|
|
4679
|
+
return Boolean(bot && !bot.hidden && task && !task.routineRunId);
|
|
4680
|
+
},
|
|
4681
|
+
resolveResultsThread: (routine, forceNew) => {
|
|
4682
|
+
// A trusted chat source is already snapshotted as sourceThreadId on each
|
|
4683
|
+
// run. Keep it distinct: it can belong to a teammate or room, whereas an
|
|
4684
|
+
// explicit resultsThreadId must be a visible task owned by the running bot.
|
|
4685
|
+
if (!forceNew && routineSourceOwner(routine))
|
|
4686
|
+
return routine.resultsThreadId;
|
|
4687
|
+
return store.createTask(routine.botId, `${routine.name} · Results`, false)?.threadId;
|
|
4688
|
+
},
|
|
4689
|
+
discardResultsThread: (botId, threadId) => {
|
|
4690
|
+
const task = store.taskByThread(botId, threadId);
|
|
4691
|
+
if (task && !task.busy && !task.routineRunId && store.messagesFor(threadId).length === 0) {
|
|
4692
|
+
store.deleteTask(botId, threadId);
|
|
4693
|
+
}
|
|
4694
|
+
},
|
|
4573
4695
|
startTurn: (botId, threadId, prompt, runOn, triggerSource, onDispatchError) => startTurn(botId, prompt, { threadId, runOn, automationSource: triggerSource, onDispatchError })
|
|
4574
4696
|
.then(() => undefined),
|
|
4575
4697
|
startGoal: async (groupId, threadId, prompt, coordinatorBotId, runId, _onDispatchError) => {
|
|
@@ -4738,6 +4860,13 @@ const agentRoutine = (routine, latestRun) => {
|
|
|
4738
4860
|
type: "interval",
|
|
4739
4861
|
everyMinutes: routine.schedule.everyMinutes,
|
|
4740
4862
|
anchorAt: new Date(routine.schedule.anchorAt).toISOString(),
|
|
4863
|
+
...(routine.schedule.weekdays === undefined
|
|
4864
|
+
? {}
|
|
4865
|
+
: { weekdays: routine.schedule.weekdays.map((day) => ROUTINE_WEEKDAY_NAMES[day]) }),
|
|
4866
|
+
...(routine.schedule.window === undefined ? {} : { window: { ...routine.schedule.window } }),
|
|
4867
|
+
...(routine.schedule.endsAt === undefined
|
|
4868
|
+
? {}
|
|
4869
|
+
: { endsAt: new Date(routine.schedule.endsAt).toISOString() }),
|
|
4741
4870
|
}
|
|
4742
4871
|
: {
|
|
4743
4872
|
type: "weekly",
|
|
@@ -5237,7 +5366,7 @@ setupRetry = 0) {
|
|
|
5237
5366
|
readyGroup.bulletin.trim() && `Room bulletin (shared instructions for everyone):\n${readyGroup.bulletin.trim()}`,
|
|
5238
5367
|
`Reply as yourself, briefly and conversationally. To bring a teammate in, mention them like @Name — they'll see the conversation and respond.`,
|
|
5239
5368
|
outsideRoom.length > 0 && roomPeerRosterSystemPrompt(outsideRoom),
|
|
5240
|
-
integrations.agents && CREDENTIAL_PROMPT.trim(),
|
|
5369
|
+
integrations.agents && (CREDENTIAL_PROMPT + THREADS_PROMPT).trim(),
|
|
5241
5370
|
integrations.agents && ROUTINE_PROMPT.trim(),
|
|
5242
5371
|
integrations.agents && PROFILE_PROMPT.trim(),
|
|
5243
5372
|
skillAuthoring && LEARN_PROMPT.trim(),
|
|
@@ -6790,6 +6919,7 @@ function cliProbeEnvironment() {
|
|
|
6790
6919
|
"OMB_COMPOSIO_BROKER_TOKEN",
|
|
6791
6920
|
"OMB_TTS_KEY",
|
|
6792
6921
|
"OMB_OPENAI_IMAGE_KEY",
|
|
6922
|
+
"OMB_CUSTOM_IMAGE_KEY",
|
|
6793
6923
|
"ANTHROPIC_API_KEY",
|
|
6794
6924
|
"OPENAI_API_KEY",
|
|
6795
6925
|
]) {
|
|
@@ -6902,7 +7032,7 @@ function configStatus() {
|
|
|
6902
7032
|
// the chosen voice is a setting, not a secret; the key is reported the
|
|
6903
7033
|
// same configured-or-not way as every other credential
|
|
6904
7034
|
tts: tts.describeVoice(cfg),
|
|
6905
|
-
imageGen:
|
|
7035
|
+
imageGen: avatarImageStatus(cfg),
|
|
6906
7036
|
// not a secret — the sidebar shows it
|
|
6907
7037
|
profile: { name: cfg.profile?.name ?? "", email: cfg.profile?.email ?? "" },
|
|
6908
7038
|
// not a secret — the settings picker shows it; "" = follow the system
|
|
@@ -7406,6 +7536,7 @@ const handleRequest = async (req, res) => {
|
|
|
7406
7536
|
...parsed.data,
|
|
7407
7537
|
generation,
|
|
7408
7538
|
createdBots: 0,
|
|
7539
|
+
openedThreads: 0,
|
|
7409
7540
|
});
|
|
7410
7541
|
return json(res, 201, { token });
|
|
7411
7542
|
}
|
|
@@ -7527,6 +7658,82 @@ const handleRequest = async (req, res) => {
|
|
|
7527
7658
|
// to keep trying — but it is still NAMED, with the refusal it would
|
|
7528
7659
|
// have met. Without that the bot can only say it is in no room at
|
|
7529
7660
|
// all, while the person is looking at it in that very room.
|
|
7661
|
+
// list_threads: the caller's own threads, plus — on every peer it can
|
|
7662
|
+
// reach — only the threads the caller itself opened. A peer's other
|
|
7663
|
+
// threads are its own business (and the person's), so the scope is
|
|
7664
|
+
// "what you started", never "what that bot is doing". State is read
|
|
7665
|
+
// the way the sidebar reads it, so the bot and the person agree.
|
|
7666
|
+
if (method === "GET" && path === "/api/internal/threads") {
|
|
7667
|
+
const from = internalSender;
|
|
7668
|
+
const fromThreadId = internalCapability.threadId;
|
|
7669
|
+
if (!connectorThread(from.id, fromThreadId)) {
|
|
7670
|
+
return json(res, 403, { error: "source conversation does not belong to sender" });
|
|
7671
|
+
}
|
|
7672
|
+
const rows = [];
|
|
7673
|
+
const stateOf = (bot, task) => {
|
|
7674
|
+
if (task.activity === "waiting-on-you")
|
|
7675
|
+
return "waiting-on-you";
|
|
7676
|
+
if (threadBusy(bot.id, task.threadId))
|
|
7677
|
+
return "running";
|
|
7678
|
+
if (queuedThreadPosition(bot.id, task.threadId) !== null)
|
|
7679
|
+
return "queued";
|
|
7680
|
+
return "idle";
|
|
7681
|
+
};
|
|
7682
|
+
for (const task of store.tasks(from.id)) {
|
|
7683
|
+
rows.push({
|
|
7684
|
+
threadId: task.threadId, botId: from.id, botName: from.name, title: task.title,
|
|
7685
|
+
state: stateOf(from, task), unread: task.unread === true, openedAt: task.openedBy?.at ?? task.createdAt,
|
|
7686
|
+
delegationId: task.openedBy?.delegationId, own: true,
|
|
7687
|
+
});
|
|
7688
|
+
}
|
|
7689
|
+
for (const peer of reachablePeers(store.bots, from)) {
|
|
7690
|
+
for (const task of store.tasks(peer.id)) {
|
|
7691
|
+
if (task.openedBy?.botId !== from.id)
|
|
7692
|
+
continue;
|
|
7693
|
+
rows.push({
|
|
7694
|
+
threadId: task.threadId, botId: peer.id, botName: peer.name, title: task.title,
|
|
7695
|
+
state: stateOf(peer, task), unread: task.unread === true, openedAt: task.openedBy.at,
|
|
7696
|
+
delegationId: task.openedBy.delegationId, own: false,
|
|
7697
|
+
});
|
|
7698
|
+
}
|
|
7699
|
+
}
|
|
7700
|
+
rows.sort((a, b) => b.openedAt - a.openedAt);
|
|
7701
|
+
return json(res, 200, { threads: rows.slice(0, 100) });
|
|
7702
|
+
}
|
|
7703
|
+
// close_thread: a bot tidies a thread it opened (or one of its own)
|
|
7704
|
+
// once its result has been read. Closing is the sidebar's idle state
|
|
7705
|
+
// plus a chip saying who closed it — never a deletion, which stays a
|
|
7706
|
+
// person's confirmed action, and never while the thread is running.
|
|
7707
|
+
const closeMatch = method === "POST" ? path.match(/^\/api\/internal\/threads\/([\w-]+)\/close$/) : null;
|
|
7708
|
+
if (closeMatch) {
|
|
7709
|
+
const from = internalSender;
|
|
7710
|
+
const fromThreadId = internalCapability.threadId;
|
|
7711
|
+
if (!connectorThread(from.id, fromThreadId)) {
|
|
7712
|
+
return json(res, 403, { error: "source conversation does not belong to sender" });
|
|
7713
|
+
}
|
|
7714
|
+
const threadId = closeMatch[1];
|
|
7715
|
+
const owner = [from, ...reachablePeers(store.bots, from)].find((bot) => store.taskByThread(bot.id, threadId));
|
|
7716
|
+
const task = owner ? store.taskByThread(owner.id, threadId) : undefined;
|
|
7717
|
+
if (!owner || !task)
|
|
7718
|
+
return json(res, 404, { error: "no such thread — call list_threads for the ones you can see" });
|
|
7719
|
+
if (owner.id !== from.id && task.openedBy?.botId !== from.id) {
|
|
7720
|
+
return json(res, 403, { error: "that thread is not yours to close — only the bot that opened it, or its own bot, can" });
|
|
7721
|
+
}
|
|
7722
|
+
if (threadId === fromThreadId)
|
|
7723
|
+
return json(res, 400, { error: "you cannot close the thread you are speaking in — finish your turn instead" });
|
|
7724
|
+
if (threadBusy(owner.id, threadId)) {
|
|
7725
|
+
return json(res, 409, { error: `#${task.title} is still running — wait for it to finish (list_threads), or the person can stop it from the app` });
|
|
7726
|
+
}
|
|
7727
|
+
store.appendMessage(threadId, {
|
|
7728
|
+
role: "bot",
|
|
7729
|
+
kind: "activity",
|
|
7730
|
+
from: { botId: from.id, name: from.name, color: from.color },
|
|
7731
|
+
tool: { name: `Closed by @${from.name}`, ok: true },
|
|
7732
|
+
});
|
|
7733
|
+
if (task.unread)
|
|
7734
|
+
store.patchTask(owner.id, threadId, { unread: false });
|
|
7735
|
+
return json(res, 200, { closed: true, threadId, title: task.title, botName: owner.name });
|
|
7736
|
+
}
|
|
7530
7737
|
if (method === "GET" && path === "/api/internal/rooms") {
|
|
7531
7738
|
const from = internalSender;
|
|
7532
7739
|
const fromThreadId = internalCapability.threadId;
|
|
@@ -8220,6 +8427,128 @@ const handleRequest = async (req, res) => {
|
|
|
8220
8427
|
store.appendMessage(fromThreadId, chip);
|
|
8221
8428
|
return json(res, 201, { ok: true, messageId: posted.id, roomName: room.name });
|
|
8222
8429
|
}
|
|
8430
|
+
// start_thread: a bot opens a real thread — on itself for separate
|
|
8431
|
+
// work, or on a teammate as a handoff that should run on its own.
|
|
8432
|
+
// Never activates: a bot must not move what the person is looking at.
|
|
8433
|
+
if (method === "POST" && path === "/api/internal/threads") {
|
|
8434
|
+
const body = await readInternalBody();
|
|
8435
|
+
const from = internalSender;
|
|
8436
|
+
const fromThreadId = internalCapability.threadId;
|
|
8437
|
+
const owner = connectorThread(from.id, fromThreadId);
|
|
8438
|
+
if (!owner)
|
|
8439
|
+
return json(res, 403, { error: "source conversation does not belong to sender" });
|
|
8440
|
+
if (body.depth !== undefined &&
|
|
8441
|
+
(!Number.isInteger(body.depth) || body.depth < 0 || body.depth !== internalCapability.depth)) {
|
|
8442
|
+
return json(res, 403, { error: "the recursion depth does not match this turn" });
|
|
8443
|
+
}
|
|
8444
|
+
const title = String(body.title ?? "").trim();
|
|
8445
|
+
const message = String(body.message ?? "").trim();
|
|
8446
|
+
if (!title || !message)
|
|
8447
|
+
return json(res, 400, { error: "title and message are required" });
|
|
8448
|
+
if (title.length > 80)
|
|
8449
|
+
return json(res, 400, { error: "title must be at most 80 characters" });
|
|
8450
|
+
// the title is quoted into chips and the sidebar, one line each
|
|
8451
|
+
if (!fitsOnOneLine(title))
|
|
8452
|
+
return json(res, 400, { error: "title must fit on one line" });
|
|
8453
|
+
if (internalCapability.openedThreads >= MAX_THREADS_OPENED_PER_TURN) {
|
|
8454
|
+
return json(res, 429, { error: `you can open at most ${MAX_THREADS_OPENED_PER_TURN} threads in one turn` });
|
|
8455
|
+
}
|
|
8456
|
+
const toBotId = typeof body.toBotId === "string" && body.toBotId.trim() ? body.toBotId.trim() : from.id;
|
|
8457
|
+
const target = store.bot(toBotId);
|
|
8458
|
+
if (!target)
|
|
8459
|
+
return json(res, 404, { error: "no such bot" });
|
|
8460
|
+
// A folder is the target's own organisation; a name is what the
|
|
8461
|
+
// model has, an id is what the sidebar has, so accept either.
|
|
8462
|
+
const folder = typeof body.folder === "string" ? body.folder.trim() : "";
|
|
8463
|
+
let projectId;
|
|
8464
|
+
if (folder) {
|
|
8465
|
+
const project = (target.projects ?? []).find((candidate) => candidate.id === folder || candidate.name.trim().toLowerCase() === folder.toLowerCase());
|
|
8466
|
+
if (!project) {
|
|
8467
|
+
const names = (target.projects ?? []).map((candidate) => candidate.name).join(", ");
|
|
8468
|
+
return json(res, 400, { error: `@${target.name} has no folder named "${folder}"${names ? ` — the folders are: ${names}` : " — it has no folders; leave folder out"}` });
|
|
8469
|
+
}
|
|
8470
|
+
projectId = project.id;
|
|
8471
|
+
}
|
|
8472
|
+
const sourceTitle = owner.group ? owner.group.name : (store.taskByThread(from.id, fromThreadId)?.title ?? "");
|
|
8473
|
+
if (target.id === from.id) {
|
|
8474
|
+
const task = store.createTask(from.id, title, false, projectId, { botId: from.id, name: from.name, at: Date.now() });
|
|
8475
|
+
if (!task)
|
|
8476
|
+
return json(res, 500, { error: "couldn't create that thread" });
|
|
8477
|
+
internalCapability.openedThreads += 1;
|
|
8478
|
+
const chip = {
|
|
8479
|
+
role: "bot",
|
|
8480
|
+
kind: "activity",
|
|
8481
|
+
tool: { name: `Opened thread #${task.title}`, ok: true },
|
|
8482
|
+
threadRef: { botId: from.id, threadId: task.threadId, title: task.title },
|
|
8483
|
+
};
|
|
8484
|
+
if (owner.group)
|
|
8485
|
+
chip.from = { botId: from.id, name: from.name, color: from.color };
|
|
8486
|
+
store.appendMessage(fromThreadId, chip);
|
|
8487
|
+
// The first line is the bot's own words. Say so where the model
|
|
8488
|
+
// reads it, so a later turn in that thread never mistakes the
|
|
8489
|
+
// request for the person's.
|
|
8490
|
+
const opening = `[Thread you opened yourself${sourceTitle ? ` from #${sourceTitle}` : ""}. The request below is your own words, not the person's: do the work here and end with a clear result they can read.]\n\n${message}`;
|
|
8491
|
+
const outcome = await startOrQueueOpenedThread(from.id, task.threadId, opening, isUnattended(from.id, fromThreadId));
|
|
8492
|
+
return json(res, 201, {
|
|
8493
|
+
threadId: task.threadId,
|
|
8494
|
+
title: task.title,
|
|
8495
|
+
botId: from.id,
|
|
8496
|
+
botName: from.name,
|
|
8497
|
+
self: true,
|
|
8498
|
+
limit: maxConcurrentBotThreads(cfg),
|
|
8499
|
+
...outcome,
|
|
8500
|
+
});
|
|
8501
|
+
}
|
|
8502
|
+
// A peer thread is a handoff into a fresh thread: every gate the
|
|
8503
|
+
// classic handoff has applies unchanged, here at queue time and
|
|
8504
|
+
// again in the drain at dispatch time.
|
|
8505
|
+
const depth = internalCapability.depth;
|
|
8506
|
+
if (depth >= MAX_COMMS_DEPTH) {
|
|
8507
|
+
return json(res, 200, { error: "thread chains are limited to one hop — open the thread on yourself, or do this one here" });
|
|
8508
|
+
}
|
|
8509
|
+
if (sectionKey(from.section) !== sectionKey(target.section)) {
|
|
8510
|
+
return json(res, 403, { error: "that bot belongs to a different section" });
|
|
8511
|
+
}
|
|
8512
|
+
if (!peerAllowed(from, target.id)) {
|
|
8513
|
+
return json(res, 403, { error: "that bot is not on this bot's allowed peers — call list_bots for the ones you can reach" });
|
|
8514
|
+
}
|
|
8515
|
+
const task = store.createTask(target.id, title, false, projectId, { botId: from.id, name: from.name, at: Date.now() });
|
|
8516
|
+
if (!task)
|
|
8517
|
+
return json(res, 500, { error: "couldn't create that thread" });
|
|
8518
|
+
const queued = queueDelegation(commsBus, from, { toBotId: target.id, message, depth, targetThreadId: task.threadId }, MAX_COMMS_DEPTH, fromThreadId);
|
|
8519
|
+
if (queued.result !== "ok" || !queued.id) {
|
|
8520
|
+
// nothing will ever run there: take the row back before the person
|
|
8521
|
+
// sees a thread that goes nowhere
|
|
8522
|
+
store.deleteTask(target.id, task.threadId);
|
|
8523
|
+
const said = {
|
|
8524
|
+
self: "a bot cannot hand a thread to itself this way — leave bot_id out",
|
|
8525
|
+
too_deep: "thread chains are limited to one hop — open the thread on yourself, or do this one here",
|
|
8526
|
+
no_target: "no such bot",
|
|
8527
|
+
too_many: "too many handoffs queued on this turn — finish your turn and open the rest next time",
|
|
8528
|
+
};
|
|
8529
|
+
return json(res, 200, { error: said[queued.result === "ok" ? "no_target" : queued.result] });
|
|
8530
|
+
}
|
|
8531
|
+
store.setTaskOpenedBy(target.id, task.threadId, { botId: from.id, name: from.name, delegationId: queued.id, at: task.openedBy?.at ?? Date.now() });
|
|
8532
|
+
internalCapability.openedThreads += 1;
|
|
8533
|
+
// An honest forecast, not a promise: the handoff starts when this
|
|
8534
|
+
// turn ends, and by then the target's slots are taken by whatever is
|
|
8535
|
+
// running there plus the threads this turn already opened ahead.
|
|
8536
|
+
const limit = maxConcurrentBotThreads(cfg);
|
|
8537
|
+
const running = store.tasks(target.id).filter((candidate) => threadBusy(target.id, candidate.threadId)).length;
|
|
8538
|
+
const ahead = pendingDelegationSnapshot().filter((pending) => pending.toBotId === target.id && pending.targetThreadId !== undefined && pending.targetThreadId !== task.threadId).length;
|
|
8539
|
+
const position = running + ahead + 1;
|
|
8540
|
+
return json(res, 201, {
|
|
8541
|
+
threadId: task.threadId,
|
|
8542
|
+
title: task.title,
|
|
8543
|
+
botId: target.id,
|
|
8544
|
+
botName: target.name,
|
|
8545
|
+
self: false,
|
|
8546
|
+
delegationId: queued.id,
|
|
8547
|
+
approvalRequired: Boolean(from.approvePeerComms),
|
|
8548
|
+
limit,
|
|
8549
|
+
...(position > limit ? { state: "queued", position: position - limit } : { state: "pending" }),
|
|
8550
|
+
});
|
|
8551
|
+
}
|
|
8223
8552
|
if (method === "POST" && path === "/api/internal/create-bot") {
|
|
8224
8553
|
const body = await readInternalBody();
|
|
8225
8554
|
const chief = internalSender;
|
|
@@ -8732,6 +9061,7 @@ const handleRequest = async (req, res) => {
|
|
|
8732
9061
|
return json(res, 400, { error: "messages must be a non-negative whole number" });
|
|
8733
9062
|
return json(res, 200, {
|
|
8734
9063
|
bots: store.bots.map((bot) => ({ ...publicBot(bot), ...messagePage(bot.threadId, limit) })),
|
|
9064
|
+
botQueuedMessages: publicBotQueuedMessages(),
|
|
8735
9065
|
groups: store.groups.map((g) => ({ ...publicGroupState(g), ...messagePage(g.threadId, limit) })),
|
|
8736
9066
|
computerControl: Object.fromEntries(store.bots.map((bot) => {
|
|
8737
9067
|
const snapshot = computerControl.snapshot(bot.id);
|
|
@@ -10024,7 +10354,7 @@ const handleRequest = async (req, res) => {
|
|
|
10024
10354
|
if (!parsed.success) {
|
|
10025
10355
|
return json(res, 400, { error: `prompt must be at most 400 characters` });
|
|
10026
10356
|
}
|
|
10027
|
-
const generated = await generateAvatarImage(cfg
|
|
10357
|
+
const generated = await generateAvatarImage(cfg, existing, parsed.data.prompt);
|
|
10028
10358
|
const current = store.bot(existing.id);
|
|
10029
10359
|
if (!current)
|
|
10030
10360
|
return json(res, 404, { error: "no such bot" });
|
|
@@ -12005,7 +12335,7 @@ const handleRequest = async (req, res) => {
|
|
|
12005
12335
|
const state = await providerAuthSessions.status(authStatus[1], auth.kind === "session" ? auth.session.id : "loopback", url.searchParams.get("flowId") ?? "");
|
|
12006
12336
|
return json(res, 200, { auth: state });
|
|
12007
12337
|
}
|
|
12008
|
-
const instanceAction = /^\/api\/instances\/([\w.-]+)\/(refresh-models|install|auth\/start|auth\/complete|auth\/cancel)$/.exec(path);
|
|
12338
|
+
const instanceAction = /^\/api\/instances\/([\w.-]+)\/(refresh-models|install|auth\/start|auth\/complete|auth\/cancel|auth\/sign-out)$/.exec(path);
|
|
12009
12339
|
if (method === "POST" && instanceAction) {
|
|
12010
12340
|
if (!String(req.headers["content-type"] ?? "").toLowerCase().startsWith("application/json")) {
|
|
12011
12341
|
return json(res, 415, { error: "content-type must be application/json" });
|
|
@@ -12023,7 +12353,7 @@ const handleRequest = async (req, res) => {
|
|
|
12023
12353
|
}
|
|
12024
12354
|
if (action === "install") {
|
|
12025
12355
|
if (!(await registry.installRuntime(instanceId)))
|
|
12026
|
-
return json(res, 404, { error: "
|
|
12356
|
+
return json(res, 404, { error: "Installing this engine from Settings is not available on this server. Use the install command on the machine running OpenMausBot." });
|
|
12027
12357
|
return json(res, 200, { instances: await describeInstances() });
|
|
12028
12358
|
}
|
|
12029
12359
|
if (action === "auth/start") {
|
|
@@ -12038,6 +12368,13 @@ const handleRequest = async (req, res) => {
|
|
|
12038
12368
|
}
|
|
12039
12369
|
return json(res, 200, { auth: started });
|
|
12040
12370
|
}
|
|
12371
|
+
if (action === "auth/sign-out") {
|
|
12372
|
+
const instance = registry.get(instanceId);
|
|
12373
|
+
if (!instance)
|
|
12374
|
+
return json(res, 404, { error: "unknown instance" });
|
|
12375
|
+
await providerAuthSessions.signOut(instance, owner);
|
|
12376
|
+
return json(res, 200, { instances: await describeInstances() });
|
|
12377
|
+
}
|
|
12041
12378
|
if (action === "auth/complete") {
|
|
12042
12379
|
const body = await readBody(req);
|
|
12043
12380
|
const flowId = typeof body?.flowId === "string" ? body.flowId : "";
|
|
@@ -12613,6 +12950,8 @@ const handleRequest = async (req, res) => {
|
|
|
12613
12950
|
persisted.tts.key = "";
|
|
12614
12951
|
if (persisted.imageGen?.key !== undefined)
|
|
12615
12952
|
persisted.imageGen.key = "";
|
|
12953
|
+
if (persisted.imageGen?.customApiKey !== undefined)
|
|
12954
|
+
persisted.imageGen.customApiKey = "";
|
|
12616
12955
|
saveConfig(persisted);
|
|
12617
12956
|
configWriteCommitted = true;
|
|
12618
12957
|
syncCredentialEnv(patch);
|