openmausbot 0.1.91 → 0.1.93
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/DesktopViewer-CKNR3SEW.js +5 -0
- package/dist/assets/{abnfDiagram-O67JEVCF-yP-w8pRq.js → abnfDiagram-O67JEVCF-Ckrm1I1X.js} +1 -1
- package/dist/assets/{arc-DsivfZEh.js → arc-BBdBuCgB.js} +1 -1
- package/dist/assets/{architectureDiagram-NJMV4G6O-CILw7lzT.js → architectureDiagram-NJMV4G6O-CDdA3Hjs.js} +1 -1
- package/dist/assets/{blockDiagram-BEXU5L5S-DEQUV3HF.js → blockDiagram-BEXU5L5S-AUrsx1YR.js} +1 -1
- package/dist/assets/{c4Diagram-YGBWAQC7-lSXCZoKA.js → c4Diagram-YGBWAQC7-BEqQ0Unh.js} +1 -1
- package/dist/assets/channel-BU94VpxE.js +1 -0
- package/dist/assets/{chunk-2Q5K7J3B-QCD5bgWP.js → chunk-2Q5K7J3B-CEEuvd4C.js} +1 -1
- package/dist/assets/{chunk-3FUC2YCW-C5-iZJuL.js → chunk-3FUC2YCW-DyANCiHl.js} +2 -2
- package/dist/assets/{chunk-5VM5RSS4-cxsawMpY.js → chunk-5VM5RSS4-Dx_Slpw9.js} +1 -1
- package/dist/assets/{chunk-GTNCS2PH-DV8tV8H7.js → chunk-GTNCS2PH-Dl7h7iiD.js} +1 -1
- package/dist/assets/{chunk-GWA4HPMP-CJFZyfbW.js → chunk-GWA4HPMP-BLx07SPb.js} +1 -1
- package/dist/assets/{chunk-JWPE2WC7-BigbMXco.js → chunk-JWPE2WC7-BMxDj3n7.js} +1 -1
- package/dist/assets/{chunk-NETBCI7D-oVxTfpDy.js → chunk-NETBCI7D-CN66PT4H.js} +1 -1
- package/dist/assets/{chunk-WEXAMYUT-CH4BiroW.js → chunk-WEXAMYUT-6HwhsDKw.js} +1 -1
- package/dist/assets/{chunk-XXDRQBXY-KEDa6gax.js → chunk-XXDRQBXY-gWnLt7SN.js} +1 -1
- package/dist/assets/{classDiagram-v2-NBCMYWYE-BiPjfqVv.js → classDiagram-v2-NBCMYWYE-CMS5l2ea.js} +1 -1
- package/dist/assets/{cose-bilkent-JH36ORCC-Caxew3OY.js → cose-bilkent-JH36ORCC-CV7sQwIG.js} +1 -1
- package/dist/assets/{cynefin-OW5HDTMX-B_Pui369.js → cynefin-OW5HDTMX-CSyl-7vl.js} +1 -1
- package/dist/assets/{cynefinDiagram-VND7K2PF-X64ydIhP.js → cynefinDiagram-VND7K2PF-BCLKL6az.js} +1 -1
- package/dist/assets/{dagre-6A5THRUB-TqYCI1E1.js → dagre-6A5THRUB-DiaZs6S1.js} +1 -1
- package/dist/assets/{diagram-22UHCM2B-TeOt3rAQ.js → diagram-22UHCM2B-BeTtmt3I.js} +1 -1
- package/dist/assets/{diagram-3UASUU5V-D70BLhDA.js → diagram-3UASUU5V-CsdBG-QQ.js} +1 -1
- package/dist/assets/{diagram-ATOU4E4O-BbQDLwUC.js → diagram-ATOU4E4O-BczvdbR1.js} +1 -1
- package/dist/assets/{diagram-CDSNMT55-ChNJZ1u-.js → diagram-CDSNMT55-DHTTTMhi.js} +1 -1
- package/dist/assets/{diagram-MLGK6HIB-DMBl2APa.js → diagram-MLGK6HIB-RTjHyd8z.js} +1 -1
- package/dist/assets/{diagram-MPIPVDR6-CEqDchOp.js → diagram-MPIPVDR6-BDenHEH_.js} +1 -1
- package/dist/assets/{ebnfDiagram-ZINNZB2B-BVDKRp_-.js → ebnfDiagram-ZINNZB2B-BSiRSbtL.js} +1 -1
- package/dist/assets/{elk-276RUBZZ-CRkofO9B.js → elk-276RUBZZ-BCAElss-.js} +1 -1
- package/dist/assets/{erDiagram-OPXOYQCR-DruOMh5e.js → erDiagram-OPXOYQCR-C4_1ZPJe.js} +1 -1
- package/dist/assets/{flowDiagram-KWPJA3E3-8GMQeB7t.js → flowDiagram-KWPJA3E3-Bcb4Z75S.js} +1 -1
- package/dist/assets/{ganttDiagram-FUAMR5RP-ORE9jcsM.js → ganttDiagram-FUAMR5RP-BiPOzxzb.js} +1 -1
- package/dist/assets/{gitGraphDiagram-X574FWY7-BmsedsWh.js → gitGraphDiagram-X574FWY7-B1wtF3ZS.js} +1 -1
- package/dist/assets/index-Biji-bfL.css +1 -0
- package/dist/assets/{index-DKNEM8Q6.js → index-BkYwilkG.js} +1 -1
- package/dist/assets/index-DZlhoES0.js +597 -0
- package/dist/assets/{infoDiagram-VRGFBTTK-E_p7AooJ.js → infoDiagram-VRGFBTTK-A19No71U.js} +1 -1
- package/dist/assets/{ishikawaDiagram-OU5B5YK6-Y-B2mKda.js → ishikawaDiagram-OU5B5YK6-BQsqJ_cV.js} +1 -1
- package/dist/assets/{journeyDiagram-ZHPQQLJL-eA8RELcK.js → journeyDiagram-ZHPQQLJL-DAO0pVan.js} +1 -1
- package/dist/assets/{kanban-definition-PNTS6WVX-DqIL2e8e.js → kanban-definition-PNTS6WVX-NepmKEe2.js} +1 -1
- package/dist/assets/{linear-yC_29R6e.js → linear-bq1PTDTW.js} +1 -1
- package/dist/assets/{mermaid.core-VhEMkSND.js → mermaid.core-s4LTbQle.js} +5 -5
- package/dist/assets/{mindmap-definition-NLK3R4M7-oJ7kyQZy.js → mindmap-definition-NLK3R4M7-LtdbgHFH.js} +1 -1
- package/dist/assets/{pegDiagram-GJSIUBJH-BmfYcY0O.js → pegDiagram-GJSIUBJH-tdbaCxiK.js} +1 -1
- package/dist/assets/{pieDiagram-5QR66LMP-DKvgP7w9.js → pieDiagram-5QR66LMP-DvkbpW8l.js} +1 -1
- package/dist/assets/{quadrantDiagram-O4NWA36T-BEKL3W0L.js → quadrantDiagram-O4NWA36T-Do4KaH-E.js} +1 -1
- package/dist/assets/{railroadDiagram-XR7U4H2S-BEsiqR5s.js → railroadDiagram-XR7U4H2S-BQWULOAs.js} +1 -1
- package/dist/assets/{requirementDiagram-PLB6GJNP-DWBcWvAP.js → requirementDiagram-PLB6GJNP-w9kei1FU.js} +1 -1
- package/dist/assets/{sankeyDiagram-IPEJSGJF-rpZ2_O4s.js → sankeyDiagram-IPEJSGJF-D2gRy0e8.js} +1 -1
- package/dist/assets/{sequenceDiagram-PO4LG4MO-B99HQdJR.js → sequenceDiagram-PO4LG4MO-a2sO5_Eo.js} +1 -1
- package/dist/assets/{sizeCapture-INFHLROL-BjN0qT5T.js → sizeCapture-INFHLROL-CiPJmFEB.js} +1 -1
- package/dist/assets/{stateDiagram-v2-GCMORJYK-BbsVgGsC.js → stateDiagram-v2-GCMORJYK-DAkUUlL7.js} +1 -1
- package/dist/assets/{swimlanes-2SLR337P-DG3gKkeR.js → swimlanes-2SLR337P-DANf2R8Y.js} +1 -1
- package/dist/assets/swimlanesDiagram-TC7HE7FX-CdHxG8aM.js +8 -0
- package/dist/assets/{timeline-definition-EJHVYXUP-BTHOdtF_.js → timeline-definition-EJHVYXUP-BKpQmzrT.js} +1 -1
- package/dist/assets/{usecaseDiagram-POWQR4AR-acLSLFwc.js → usecaseDiagram-POWQR4AR-BtI7qLN-.js} +1 -1
- package/dist/assets/{vennDiagram-UO4OBE2U-C5JzNX9G.js → vennDiagram-UO4OBE2U-gXJXrAG_.js} +1 -1
- package/dist/assets/{wardleyDiagram-VNRHLVJA-DL1-PrR_.js → wardleyDiagram-VNRHLVJA-GU8Po5-n.js} +1 -1
- package/dist/assets/{xychartDiagram-PMCCYNJV-Bvr1477p.js → xychartDiagram-PMCCYNJV-BF4J30g6.js} +1 -1
- package/dist/index.html +2 -2
- package/dist/novnc-NOTICE.txt +118 -0
- package/dist-server/cloud-home-start.js +112 -14
- package/dist-server/companion/src/routes.js +24 -1
- package/dist-server/container-mcp.js +201 -16
- package/dist-server/drivers/agents-proxy.js +7 -4
- package/dist-server/enterprise/server/index.js +5 -1
- package/dist-server/index.js +10698 -4737
- package/dist-server/local-computer-proxy.js +133 -8
- package/dist-server/mcp-gate.js +16 -6
- package/dist-server/model-catalog/models-dev.snapshot.json +7299 -0
- package/dist-server/openmausbot.js +4656 -1258
- package/dist-server/pair-cli.js +4656 -1258
- package/dist-server/server/boat.js +5 -5
- package/dist-server/server/browser-engine.js +10 -1
- package/dist-server/server/browser-live.js +46 -2
- package/dist-server/server/browser-runtime.js +74 -8
- package/dist-server/server/browser-tool-shape.js +6 -4
- package/dist-server/server/chief-of-staff.js +1 -1
- package/dist-server/server/cloud-home-start.js +118 -26
- package/dist-server/server/cloud-home.js +39 -3
- package/dist-server/server/cloud-lending.js +74 -10
- package/dist-server/server/cloud-owner.js +196 -0
- package/dist-server/server/cloud-secrets-boot.js +6 -0
- package/dist-server/server/cloud-secrets.js +65 -0
- package/dist-server/server/computer-control.js +6 -0
- package/dist-server/server/config.js +187 -14
- package/dist-server/server/container-computer.js +46 -34
- package/dist-server/server/desktop-viewer-targets.js +26 -0
- package/dist-server/server/digest.js +12 -2
- package/dist-server/server/drivers/acp/core.js +339 -33
- package/dist-server/server/drivers/acp/cursor.js +13 -1
- package/dist-server/server/drivers/acp/opencode-go.js +614 -85
- package/dist-server/server/drivers/acp/quiet-status.js +257 -0
- package/dist-server/server/drivers/acp/qwen.js +18 -0
- package/dist-server/server/drivers/agents-catalog.js +6 -6
- package/dist-server/server/drivers/antigravity-acp.js +69 -11
- package/dist-server/server/drivers/antigravity-temp.js +360 -0
- package/dist-server/server/drivers/antigravity.js +21 -0
- package/dist-server/server/drivers/builtIn.js +2 -0
- package/dist-server/server/drivers/cerebras.js +108 -0
- package/dist-server/server/drivers/chat-mcp-tools.js +20 -3
- package/dist-server/server/drivers/chatgpt-plan-auth.js +481 -0
- package/dist-server/server/drivers/claude.js +329 -37
- package/dist-server/server/drivers/codex.js +228 -18
- package/dist-server/server/drivers/grok.js +2 -2
- package/dist-server/server/drivers/mistral.js +4 -3
- package/dist-server/server/drivers/openai-chat-protocol.js +4 -3
- package/dist-server/server/drivers/openai-chat.js +66 -4
- package/dist-server/server/drivers/openai-compat.js +73 -23
- package/dist-server/server/drivers/prompt-split.js +2 -0
- package/dist-server/server/engine-install.js +46 -12
- package/dist-server/server/env-path.js +21 -5
- package/dist-server/server/harness/registry.js +2 -2
- package/dist-server/server/index.js +1545 -238
- package/dist-server/server/lending-memory.js +479 -0
- package/dist-server/server/local-vm-idle.js +24 -6
- package/dist-server/server/local-vm-stop-reason.js +26 -0
- package/dist-server/server/managed-desktop.js +3 -0
- package/dist-server/server/mcp-bridge.js +5 -3
- package/dist-server/server/mcp-http.js +6 -3
- package/dist-server/server/mcp-oauth-discovery.js +133 -0
- package/dist-server/server/mcp-oauth-store.js +70 -0
- package/dist-server/server/mcp-oauth.js +550 -0
- package/dist-server/server/mcp-probe.js +14 -1
- package/dist-server/server/mcp-registry.js +43 -0
- package/dist-server/server/memory-upkeep.js +94 -84
- package/dist-server/server/model-catalog/catalog.js +374 -0
- package/dist-server/server/model-catalog/presets.js +176 -0
- package/dist-server/server/model-catalog/trim.js +219 -0
- package/dist-server/server/provider-key-check.js +5 -2
- package/dist-server/server/recent-work.js +1 -1
- package/dist-server/server/request-auth.js +23 -0
- package/dist-server/server/routes/antigravity-leftovers.js +45 -0
- package/dist-server/server/routes/bot-memory.js +31 -6
- package/dist-server/server/routes/desktop-viewer.js +238 -0
- package/dist-server/server/routine-requests.js +1 -1
- package/dist-server/server/routines.js +6 -3
- package/dist-server/server/sessions.js +56 -9
- package/dist-server/server/skills.js +25 -10
- package/dist-server/server/store.js +116 -122
- package/dist-server/server/thread-events.js +2 -0
- package/dist-server/server/thread-starters.js +33 -0
- package/dist-server/server/thread-title.js +46 -0
- package/dist-server/server/tts/fish.js +4 -5
- package/dist-server/server/tts/index.js +5 -3
- package/dist-server/server/vps-computer.js +80 -13
- package/dist-server/server/webhooks.js +18 -1
- package/dist-server/server/workspace-backup-policy.js +3 -0
- package/dist-server/server/workspace-backup.js +25 -0
- package/dist-server/server/workspace.js +66 -16
- package/dist-server/shared/approval-mode.js +1 -1
- package/dist-server/shared/redact.js +1 -0
- package/dist-server/shared/tool-name.js +22 -0
- package/dist-server/shared/workspace-backup-client.js +1 -0
- package/dist-server/src/lib/citations.js +177 -0
- package/dist-server/src/lib/composer-attachments.js +7 -1
- package/dist-server/vps-container-mcp.js +202 -16
- package/dist-server/workspace-backup.worker.js +87 -9
- package/enterprise/server/index.js +5 -1
- package/package.json +1 -1
- package/dist/assets/channel-DrfGMu7X.js +0 -1
- package/dist/assets/index-BlnCEcU6.js +0 -588
- package/dist/assets/index-DVxjOvID.css +0 -1
- package/dist/assets/swimlanesDiagram-TC7HE7FX-BCZhxl7e.js +0 -8
|
@@ -442,6 +442,10 @@ export function roomResponders(text, members, defaultResponder) {
|
|
|
442
442
|
}
|
|
443
443
|
return [];
|
|
444
444
|
}
|
|
445
|
+
/** A hidden routine execution is not a conversation the person can open. */
|
|
446
|
+
function taskCountsAsBotUnread(task) {
|
|
447
|
+
return Boolean(task.unread) && !task.routineRunId;
|
|
448
|
+
}
|
|
445
449
|
export class Store {
|
|
446
450
|
bots = [];
|
|
447
451
|
groups = [];
|
|
@@ -463,8 +467,13 @@ export class Store {
|
|
|
463
467
|
mkdirSync(DATA_DIR, { recursive: true });
|
|
464
468
|
for (const file of [BOTS_FILE, GROUPS_FILE])
|
|
465
469
|
tightenRegistryFile(file);
|
|
470
|
+
// Whether the bot list is the real one. Room repair below trusts it to
|
|
471
|
+
// say which members no longer exist; an unreadable file must never read
|
|
472
|
+
// as "every member was deleted".
|
|
473
|
+
let botsLoaded = false;
|
|
466
474
|
try {
|
|
467
475
|
this.bots = JSON.parse(readFileSync(BOTS_FILE, "utf8"));
|
|
476
|
+
botsLoaded = Array.isArray(this.bots);
|
|
468
477
|
}
|
|
469
478
|
catch {
|
|
470
479
|
this.bots = [];
|
|
@@ -633,9 +642,17 @@ export class Store {
|
|
|
633
642
|
botsMigrated = true;
|
|
634
643
|
}
|
|
635
644
|
}
|
|
645
|
+
const botIds = new Set(this.bots.map((b) => b.id));
|
|
636
646
|
for (const g of this.groups) {
|
|
637
647
|
g.busyBotId = null;
|
|
638
648
|
delete g.turnStartedAt;
|
|
649
|
+
// A deleted bot used to stay a member for good: counted on the room's
|
|
650
|
+
// Save button and refused by the roster check on every save (MOCA-264).
|
|
651
|
+
// Bot-to-bot channels keep their pair; they are not edited as rooms.
|
|
652
|
+
if (botsLoaded && !g.dm && g.memberIds.some((id) => !botIds.has(id))) {
|
|
653
|
+
g.memberIds = g.memberIds.filter((id) => botIds.has(id));
|
|
654
|
+
groupsMigrated = true;
|
|
655
|
+
}
|
|
639
656
|
const normalized = normalizeGroupDefaultResponder(g.defaultResponder, g.memberIds, Boolean(g.dm));
|
|
640
657
|
if (JSON.stringify(normalized) !== JSON.stringify(g.defaultResponder))
|
|
641
658
|
groupsMigrated = true;
|
|
@@ -721,6 +738,13 @@ export class Store {
|
|
|
721
738
|
task.unread = task === active && b.unread;
|
|
722
739
|
botsMigrated = true;
|
|
723
740
|
}
|
|
741
|
+
// Older builds left a failed hidden run unread, which kept the bot
|
|
742
|
+
// dot on after every visible chat was read. Clear it before the
|
|
743
|
+
// recompute below so the saved roster heals.
|
|
744
|
+
if (task.routineRunId && task.unread) {
|
|
745
|
+
task.unread = false;
|
|
746
|
+
botsMigrated = true;
|
|
747
|
+
}
|
|
724
748
|
if (task === active) {
|
|
725
749
|
if (task.rewound === undefined && b.rewound !== undefined) {
|
|
726
750
|
task.rewound = b.rewound;
|
|
@@ -742,7 +766,7 @@ export class Store {
|
|
|
742
766
|
task.turnStartedAt = undefined;
|
|
743
767
|
}
|
|
744
768
|
this.mirrorActiveTask(b, active);
|
|
745
|
-
b.unread = b.tasks.some(
|
|
769
|
+
b.unread = b.tasks.some(taskCountsAsBotUnread);
|
|
746
770
|
}
|
|
747
771
|
if (botsMigrated)
|
|
748
772
|
this.saveBots();
|
|
@@ -819,13 +843,20 @@ export class Store {
|
|
|
819
843
|
if (groupsDirty)
|
|
820
844
|
this.saveGroups();
|
|
821
845
|
}
|
|
846
|
+
/** Resolve the identity of a historical pair row, including a sender
|
|
847
|
+
* recreated with the same unique name. */
|
|
848
|
+
openerIdentity(openedBy) {
|
|
849
|
+
if (this.bot(openedBy.botId))
|
|
850
|
+
return openedBy.botId;
|
|
851
|
+
const named = this.bots.filter((bot) => bot.name === openedBy.name);
|
|
852
|
+
return named.length === 1 ? named[0].id : openedBy.botId;
|
|
853
|
+
}
|
|
822
854
|
/** At most one live pair row per (recipient, sender identity). Servers
|
|
823
855
|
* before identity-stable matching minted a second live pair row for a
|
|
824
856
|
* deleted-and-recreated sender while the predecessor's row dangled live
|
|
825
|
-
* forever; collapse those on load by keeping the row the
|
|
826
|
-
*
|
|
827
|
-
* the rest to closed plain threads. History is kept, never deleted
|
|
828
|
-
* a demoted row is never re-adopted: adoption skips closed rows. Runs on
|
|
857
|
+
* forever; collapse those on load by keeping the first row in the task
|
|
858
|
+
* list (the newest, actively used one) and demoting
|
|
859
|
+
* the rest to closed plain threads. History is kept, never deleted. Runs on
|
|
829
860
|
* every load and touches nothing in a store that already holds the
|
|
830
861
|
* invariant. */
|
|
831
862
|
repairDuplicatePairConversations() {
|
|
@@ -840,8 +871,7 @@ export class Store {
|
|
|
840
871
|
pairs.set(identity, [...(pairs.get(identity) ?? []), task]);
|
|
841
872
|
}
|
|
842
873
|
for (const rows of pairs.values()) {
|
|
843
|
-
//
|
|
844
|
-
// find() favors: the first is the one it keeps using.
|
|
874
|
+
// Rows are in task-list order; keep the newest.
|
|
845
875
|
for (const duplicate of rows.slice(1)) {
|
|
846
876
|
const by = duplicate.openedBy;
|
|
847
877
|
const identity = this.openerIdentity(by);
|
|
@@ -856,8 +886,7 @@ export class Store {
|
|
|
856
886
|
}
|
|
857
887
|
}
|
|
858
888
|
};
|
|
859
|
-
//
|
|
860
|
-
// recipient, and group tasks carry no peer stamps.
|
|
889
|
+
// Historical pair rows lived only on bots; group tasks have no peer stamps.
|
|
861
890
|
for (const bot of this.bots)
|
|
862
891
|
sweep(bot.tasks, () => { botsDirty = true; });
|
|
863
892
|
if (botsDirty)
|
|
@@ -1748,6 +1777,27 @@ export class Store {
|
|
|
1748
1777
|
this.saveBots(nextBots);
|
|
1749
1778
|
this.bots = nextBots;
|
|
1750
1779
|
this.legacyActivities.delete(id);
|
|
1780
|
+
// A deleted bot leaves every room it was in, and a room it led falls back
|
|
1781
|
+
// to its next member. Bot-to-bot channels keep their pair.
|
|
1782
|
+
const rooms = this.groups.filter((g) => !g.dm && g.memberIds.includes(id));
|
|
1783
|
+
for (const g of rooms) {
|
|
1784
|
+
g.memberIds = g.memberIds.filter((member) => member !== id);
|
|
1785
|
+
if (g.busyBotId === id) {
|
|
1786
|
+
g.busyBotId = null;
|
|
1787
|
+
delete g.turnStartedAt;
|
|
1788
|
+
}
|
|
1789
|
+
g.defaultResponder = normalizeGroupDefaultResponder(g.defaultResponder, g.memberIds, false);
|
|
1790
|
+
}
|
|
1791
|
+
if (rooms.length) {
|
|
1792
|
+
// Bot removal is already durable. Finish erasing its data even if this
|
|
1793
|
+
// write fails; startup repair removes these stale memberships later.
|
|
1794
|
+
try {
|
|
1795
|
+
this.saveGroups();
|
|
1796
|
+
}
|
|
1797
|
+
catch (error) {
|
|
1798
|
+
console.warn("store: room cleanup will retry on restart", error);
|
|
1799
|
+
}
|
|
1800
|
+
}
|
|
1751
1801
|
// every task's transcript goes with the bot, not just the open one
|
|
1752
1802
|
for (const threadId of new Set([bot.threadId, ...(bot.tasks ?? []).map((t) => t.threadId)])) {
|
|
1753
1803
|
this.deleteThreadRecord(threadId);
|
|
@@ -1770,6 +1820,8 @@ export class Store {
|
|
|
1770
1820
|
// The bot folder (SOUL.md mirror) is the bot's too.
|
|
1771
1821
|
removeBotFolder(id);
|
|
1772
1822
|
this.emit({ type: "bot.deleted", botId: id });
|
|
1823
|
+
for (const g of rooms)
|
|
1824
|
+
this.emit({ type: "group", groupId: g.id });
|
|
1773
1825
|
return true;
|
|
1774
1826
|
}
|
|
1775
1827
|
patchBot(id, patch) {
|
|
@@ -1796,7 +1848,7 @@ export class Store {
|
|
|
1796
1848
|
Object.assign(task, { [key]: structuredClone(patch[key]) });
|
|
1797
1849
|
}
|
|
1798
1850
|
}
|
|
1799
|
-
bot.unread = bot.tasks.some(
|
|
1851
|
+
bot.unread = bot.tasks.some(taskCountsAsBotUnread);
|
|
1800
1852
|
}
|
|
1801
1853
|
this.saveBots();
|
|
1802
1854
|
this.emit({ type: "bot", botId: id });
|
|
@@ -2146,11 +2198,34 @@ export class Store {
|
|
|
2146
2198
|
* current folder — unless the task already has a session (a thread from
|
|
2147
2199
|
* before folders existed), which pins to the default so the folder can't
|
|
2148
2200
|
* move under it. Returns the pinned value: a path, or null for default. */
|
|
2201
|
+
/** Forget where a conversation works: its next turn pins it again
|
|
2202
|
+
* (pinTaskCwd). For a Cloud home's conversation a revoked session opened,
|
|
2203
|
+
* whose next turn must work in its own folder (server/cloud-owner.ts). */
|
|
2204
|
+
unpinTaskCwd(threadId) {
|
|
2205
|
+
const bot = this.botByThread(threadId);
|
|
2206
|
+
const task = bot ? this.taskByThread(bot.id, threadId) : undefined;
|
|
2207
|
+
if (!bot || !task || task.cwd === undefined)
|
|
2208
|
+
return false;
|
|
2209
|
+
delete task.cwd;
|
|
2210
|
+
this.saveBots();
|
|
2211
|
+
this.emit({ type: "bot", botId: bot.id });
|
|
2212
|
+
return true;
|
|
2213
|
+
}
|
|
2149
2214
|
pinTaskCwd(botId, threadId, fallbackCwd, opts = {}) {
|
|
2150
2215
|
const bot = this.bot(botId);
|
|
2151
2216
|
const task = bot ? this.taskByThread(botId, threadId) : undefined;
|
|
2152
2217
|
if (!bot || !task)
|
|
2153
2218
|
return null;
|
|
2219
|
+
// Its own folder, never the bot's project folder (a Cloud home's guest
|
|
2220
|
+
// conversation), pinned when it first runs. A conversation that already
|
|
2221
|
+
// ran elsewhere keeps its pin: it was not opened that way, and moving it
|
|
2222
|
+
// would lose its work and its session.
|
|
2223
|
+
if (opts.privateOnly && task.cwd === undefined) {
|
|
2224
|
+
task.cwd = fallbackCwd ?? null;
|
|
2225
|
+
this.saveBots();
|
|
2226
|
+
this.emit({ type: "bot", botId });
|
|
2227
|
+
return task.cwd;
|
|
2228
|
+
}
|
|
2154
2229
|
if (opts.none) {
|
|
2155
2230
|
if (task.cwd !== null) {
|
|
2156
2231
|
task.cwd = null;
|
|
@@ -2318,9 +2393,13 @@ export class Store {
|
|
|
2318
2393
|
task.title = patch.title.trim().slice(0, 80) || UNTITLED_THREAD;
|
|
2319
2394
|
if (Object.prototype.hasOwnProperty.call(patch, "pinned") && task.pinned !== true)
|
|
2320
2395
|
delete task.pinned;
|
|
2396
|
+
// Still hidden: attention belongs on the source conversation. A same-call
|
|
2397
|
+
// clear of routineRunId (no-source promotion) may keep unread.
|
|
2398
|
+
if (task.routineRunId)
|
|
2399
|
+
task.unread = false;
|
|
2321
2400
|
if (bot.threadId === threadId)
|
|
2322
2401
|
this.mirrorActiveTask(bot, task);
|
|
2323
|
-
bot.unread = bot.tasks.some(
|
|
2402
|
+
bot.unread = bot.tasks.some(taskCountsAsBotUnread);
|
|
2324
2403
|
this.saveBots();
|
|
2325
2404
|
this.emit({ type: "bot", botId });
|
|
2326
2405
|
return task;
|
|
@@ -2392,6 +2471,24 @@ export class Store {
|
|
|
2392
2471
|
this.emit({ type: "bot", botId });
|
|
2393
2472
|
return bot;
|
|
2394
2473
|
}
|
|
2474
|
+
/** Copy this bot's saved approval level and standing approvals onto one
|
|
2475
|
+
* thread. A new thread already gets them; this is how an existing
|
|
2476
|
+
* conversation catches up. Other threads, the transcript, and the bot
|
|
2477
|
+
* default stay put. A grant that has not committed yet is ignored, so a
|
|
2478
|
+
* refresh cannot copy the temporary Ask mask. */
|
|
2479
|
+
refreshTaskPermissions(botId, threadId) {
|
|
2480
|
+
const bot = this.bot(botId);
|
|
2481
|
+
const task = this.taskByThread(botId, threadId);
|
|
2482
|
+
if (!bot || !task)
|
|
2483
|
+
return null;
|
|
2484
|
+
const mode = approvalModeFor({ ...bot, approvalGrant: undefined });
|
|
2485
|
+
const alwaysAllow = structuredClone(bot.alwaysAllow ?? []);
|
|
2486
|
+
const autoApprove = mode === "auto";
|
|
2487
|
+
const sameAllow = JSON.stringify(task.alwaysAllow ?? []) === JSON.stringify(alwaysAllow);
|
|
2488
|
+
if (task.approvalMode === mode && task.autoApprove === autoApprove && sameAllow)
|
|
2489
|
+
return task;
|
|
2490
|
+
return this.patchTask(botId, threadId, { approvalMode: mode, autoApprove, alwaysAllow });
|
|
2491
|
+
}
|
|
2395
2492
|
mirrorActiveTask(bot, task) {
|
|
2396
2493
|
bot.threadId = task.threadId;
|
|
2397
2494
|
bot.resumeCursors = structuredClone(task.resumeCursors);
|
|
@@ -2399,8 +2496,10 @@ export class Store {
|
|
|
2399
2496
|
bot.pinnedMessageId = task.pinnedMessageId;
|
|
2400
2497
|
}
|
|
2401
2498
|
/** A fresh context on the same bot: new thread, new session, same
|
|
2402
|
-
* persona/tools/computer. Becomes the active task.
|
|
2403
|
-
|
|
2499
|
+
* persona/tools/computer. Becomes the active task. It runs on the bot's
|
|
2500
|
+
* default model unless the caller hands it one (a thread opened from
|
|
2501
|
+
* another of this bot's threads keeps that thread's model). */
|
|
2502
|
+
createTask(botId, title, activate = true, projectId, openedBy, approvalMode, modelSelection) {
|
|
2404
2503
|
const bot = this.bot(botId);
|
|
2405
2504
|
if (!bot)
|
|
2406
2505
|
return null;
|
|
@@ -2415,7 +2514,7 @@ export class Store {
|
|
|
2415
2514
|
...(projectId ? { projectId } : {}),
|
|
2416
2515
|
...(openedBy ? { openedBy: structuredClone(openedBy) } : {}),
|
|
2417
2516
|
resumeCursors: {},
|
|
2418
|
-
modelSelection: structuredClone(bot.modelSelection),
|
|
2517
|
+
modelSelection: structuredClone(modelSelection ?? bot.modelSelection),
|
|
2419
2518
|
approvalMode: approvalMode ?? approvalModeFor(bot),
|
|
2420
2519
|
autoApprove: approvalMode ? false : Boolean(bot.autoApprove),
|
|
2421
2520
|
alwaysAllow: approvalMode ? [] : [...(bot.alwaysAllow ?? [])],
|
|
@@ -2463,112 +2562,6 @@ export class Store {
|
|
|
2463
2562
|
this.emit({ type: "bot", botId });
|
|
2464
2563
|
return task;
|
|
2465
2564
|
}
|
|
2466
|
-
/** Where a bot-to-bot send outside a room lands: the PAIR CONVERSATION
|
|
2467
|
-
* for (sender, recipient) — the recipient's task stamped `openedBy` this
|
|
2468
|
-
* sender with kind "pair".
|
|
2469
|
-
*
|
|
2470
|
-
* Its scope is global for those two bots: deliberately not per source
|
|
2471
|
-
* thread and not per assignment, so a teammate you work with all day is
|
|
2472
|
-
* one readable row in the recipient's sidebar that remembers what was
|
|
2473
|
-
* asked last time, instead of one row per message. Nothing about the
|
|
2474
|
-
* caller's current turn takes part in choosing it — no dispatch
|
|
2475
|
-
* generation, no request key — and never the recipient's selected
|
|
2476
|
-
* thread, which belongs to the person.
|
|
2477
|
-
*
|
|
2478
|
-
* Two things bend that rule, both deliberately:
|
|
2479
|
-
*
|
|
2480
|
-
* adoption — a recipient still carrying threads this sender opened
|
|
2481
|
-
* before pair conversations existed (one per assignment, each titled
|
|
2482
|
-
* with a sliced brief) has its most recently active one stamped as the
|
|
2483
|
-
* pair conversation instead of gaining yet another row, so the sprawl
|
|
2484
|
-
* stops on upgrade day. Nothing is deleted or closed. A start_thread
|
|
2485
|
-
* handoff is left alone: the sender named that job itself and tracks
|
|
2486
|
-
* it by its own delegation id.
|
|
2487
|
-
*
|
|
2488
|
-
* concurrency — a second assignment arriving while the pair
|
|
2489
|
-
* conversation is still working (`working`, which the caller answers
|
|
2490
|
-
* from live turn state) gets its own work thread, so two jobs never
|
|
2491
|
-
* interleave in one transcript. `label` names that thread; the caller
|
|
2492
|
-
* closes it once its result has been reported. A pair conversation
|
|
2493
|
-
* never auto-closes. */
|
|
2494
|
-
/** The identity a peer-opened row belongs to: its opener's id while that
|
|
2495
|
-
* bot lives, else the one live bot the stamp's name still points at —
|
|
2496
|
-
* what a deleted-and-recreated same-name bot inherits — else the dead id
|
|
2497
|
-
* itself. A name two live bots share resolves to nobody's twin:
|
|
2498
|
-
* ambiguous means unmatched, never a wrong merge. */
|
|
2499
|
-
openerIdentity(openedBy) {
|
|
2500
|
-
if (this.bot(openedBy.botId))
|
|
2501
|
-
return openedBy.botId;
|
|
2502
|
-
const named = this.bots.filter((bot) => bot.name === openedBy.name);
|
|
2503
|
-
return named.length === 1 ? named[0].id : openedBy.botId;
|
|
2504
|
-
}
|
|
2505
|
-
resolvePairConversation(sender, recipientId, options) {
|
|
2506
|
-
if (!this.bot(recipientId))
|
|
2507
|
-
return null;
|
|
2508
|
-
const title = `@${sender.name}`;
|
|
2509
|
-
const opener = (kind, at = Date.now()) => ({ botId: sender.id, name: sender.name, kind, at });
|
|
2510
|
-
// Identity-stable: the sender's own rows, plus ones a deleted
|
|
2511
|
-
// predecessor opened when the stamp's name still points at exactly
|
|
2512
|
-
// this bot — a same-name recreation inherits the conversation instead
|
|
2513
|
-
// of minting a twin while the old row dangles live. A name two live
|
|
2514
|
-
// bots share matches nobody's inheritance: refusing the fallback can
|
|
2515
|
-
// cost a new row, never merge two bots' histories.
|
|
2516
|
-
const fromSender = this.tasks(recipientId).filter((task) => task.openedBy && this.openerIdentity(task.openedBy) === sender.id);
|
|
2517
|
-
let pair = fromSender.find((task) => task.openedBy?.kind === "pair");
|
|
2518
|
-
if (!pair) {
|
|
2519
|
-
const lastActivity = (task) => this.messagesTail(task.threadId, 1).messages.at(-1)?.at ?? task.openedBy?.at ?? task.createdAt;
|
|
2520
|
-
const adopted = fromSender
|
|
2521
|
-
.filter((task) => !task.openedBy?.kind && !task.openedBy?.delegationId && !task.closedBy)
|
|
2522
|
-
.sort((a, b) => lastActivity(b) - lastActivity(a))[0];
|
|
2523
|
-
if (adopted) {
|
|
2524
|
-
// Keep the hour it was really opened: list_threads and the sidebar
|
|
2525
|
-
// order by it, and adoption is not a new conversation.
|
|
2526
|
-
this.setTaskOpenedBy(recipientId, adopted.threadId, opener("pair", adopted.openedBy?.at ?? adopted.createdAt));
|
|
2527
|
-
// The title changes only when nobody typed it. The rule: rename it
|
|
2528
|
-
// when it still equals what createTask made of the assignment that
|
|
2529
|
-
// opened the thread — and that assignment is still the thread's
|
|
2530
|
-
// first message, "@Recipient <brief>" — so the comparison is
|
|
2531
|
-
// threadTitleFrom(that brief). Anything else is a name a person
|
|
2532
|
-
// chose, and a thread with no request to read (its handoff never
|
|
2533
|
-
// ran) cannot be checked, so both keep the title they have.
|
|
2534
|
-
if (adopted.title === this.openingRequestTitle(recipientId, adopted.threadId)) {
|
|
2535
|
-
this.renameTask(recipientId, adopted.threadId, title);
|
|
2536
|
-
}
|
|
2537
|
-
pair = adopted;
|
|
2538
|
-
}
|
|
2539
|
-
}
|
|
2540
|
-
if (pair && !options.working(pair.threadId)) {
|
|
2541
|
-
// A conversation the sender closed after reading a result is picked
|
|
2542
|
-
// back up, never replaced: closing is only the sidebar's idle state.
|
|
2543
|
-
if (pair.closedBy)
|
|
2544
|
-
this.setTaskClosedBy(recipientId, pair.threadId, null);
|
|
2545
|
-
// An inherited row carries the predecessor's id; rebind it to the
|
|
2546
|
-
// live bot so the name fallback is needed only once — a namesake
|
|
2547
|
-
// appearing later cannot claim the row. The hour it was opened
|
|
2548
|
-
// stays: inheritance is not a new conversation.
|
|
2549
|
-
const inherited = pair.openedBy;
|
|
2550
|
-
if (inherited && inherited.botId !== sender.id) {
|
|
2551
|
-
this.setTaskOpenedBy(recipientId, pair.threadId, { ...inherited, botId: sender.id, name: sender.name });
|
|
2552
|
-
}
|
|
2553
|
-
return { task: pair, created: false };
|
|
2554
|
-
}
|
|
2555
|
-
// The brief is never a title. An 80-character slice of an assignment
|
|
2556
|
-
// is the row nobody can read, and a durable conversation outlives the
|
|
2557
|
-
// one brief that opened it.
|
|
2558
|
-
const task = this.createTask(recipientId, pair ? `${title} · ${options.label || "parallel work"}` : title, false, undefined, opener(pair ? "work" : "pair"));
|
|
2559
|
-
return task ? { task, created: true } : null;
|
|
2560
|
-
}
|
|
2561
|
-
/** The title a peer-opened thread was born with: what createTask made of
|
|
2562
|
-
* the request that opened it, which is still the first message in it,
|
|
2563
|
-
* addressed "@Recipient <brief>". null when there is no such message to
|
|
2564
|
-
* read — an unrun handoff proves nothing about who named the row. */
|
|
2565
|
-
openingRequestTitle(recipientId, threadId) {
|
|
2566
|
-
const first = this.messagesFor(threadId)[0]?.text?.trim();
|
|
2567
|
-
if (!first)
|
|
2568
|
-
return null;
|
|
2569
|
-
const addressed = `@${this.bot(recipientId)?.name ?? ""} `;
|
|
2570
|
-
return threadTitleFrom(first.startsWith(addressed) ? first.slice(addressed.length) : first);
|
|
2571
|
-
}
|
|
2572
2565
|
switchTask(botId, threadId) {
|
|
2573
2566
|
const bot = this.bot(botId);
|
|
2574
2567
|
const task = bot?.tasks?.find((t) => t.threadId === threadId);
|
|
@@ -2598,7 +2591,8 @@ export class Store {
|
|
|
2598
2591
|
/** Swap a machine-made first-message title for a generated one, once.
|
|
2599
2592
|
* Equality against the snippet is the whole contract: a rename by the
|
|
2600
2593
|
* person, by pair adoption, or by an earlier generated title each break
|
|
2601
|
-
* it, so this never overwrites a name anyone chose.
|
|
2594
|
+
* it, so this never overwrites a name anyone chose. Regenerate title passes
|
|
2595
|
+
* the title it started from, so a rename made while it ran stands. */
|
|
2602
2596
|
retitleTask(botId, threadId, machineTitle, title) {
|
|
2603
2597
|
const task = this.taskByThread(botId, threadId);
|
|
2604
2598
|
if (!task || task.title !== machineTitle)
|
|
@@ -2620,7 +2614,7 @@ export class Store {
|
|
|
2620
2614
|
this.mirrorActiveTask(bot, visible);
|
|
2621
2615
|
}
|
|
2622
2616
|
this.deleteThreadRecord(threadId);
|
|
2623
|
-
bot.unread = bot.tasks.some(
|
|
2617
|
+
bot.unread = bot.tasks.some(taskCountsAsBotUnread);
|
|
2624
2618
|
this.refreshBotActivity(bot);
|
|
2625
2619
|
this.saveBots();
|
|
2626
2620
|
this.emit({ type: "bot", botId });
|
|
@@ -215,6 +215,8 @@ function isRuntimeEvent(value) {
|
|
|
215
215
|
value.source === "peer"));
|
|
216
216
|
case "thread.token-usage.updated":
|
|
217
217
|
return typeof value.input === "number" && typeof value.output === "number";
|
|
218
|
+
case "runtime.notice":
|
|
219
|
+
return typeof value.message === "string";
|
|
218
220
|
case "runtime.error":
|
|
219
221
|
return typeof value.message === "string" &&
|
|
220
222
|
(value.setup === undefined || typeof value.setup === "boolean") &&
|
|
@@ -35,6 +35,39 @@ export class ThreadStarters {
|
|
|
35
35
|
get(threadId) {
|
|
36
36
|
return this.starters.get(threadId);
|
|
37
37
|
}
|
|
38
|
+
/** The threads opened for one person, newest first, at most `limit`. */
|
|
39
|
+
threadsOf(person, limit) {
|
|
40
|
+
const threads = [];
|
|
41
|
+
for (const [threadId, starter] of [...this.starters].reverse()) {
|
|
42
|
+
if (starter !== person)
|
|
43
|
+
continue;
|
|
44
|
+
threads.push(threadId);
|
|
45
|
+
if (threads.length >= limit)
|
|
46
|
+
break;
|
|
47
|
+
}
|
|
48
|
+
return threads;
|
|
49
|
+
}
|
|
50
|
+
/** Everyone a thread here was opened for. */
|
|
51
|
+
people() {
|
|
52
|
+
return new Set(this.starters.values());
|
|
53
|
+
}
|
|
54
|
+
/** Name `to` for every thread whose person `move` accepts: a one-time
|
|
55
|
+
* migration (server/cloud-owner.ts), not an ordinary record. The threads
|
|
56
|
+
* that changed. */
|
|
57
|
+
reassign(move, to) {
|
|
58
|
+
if (!KEY.test(to))
|
|
59
|
+
return [];
|
|
60
|
+
const moved = [];
|
|
61
|
+
for (const [threadId, person] of this.starters) {
|
|
62
|
+
if (person === to || !move(person))
|
|
63
|
+
continue;
|
|
64
|
+
this.starters.set(threadId, to);
|
|
65
|
+
moved.push(threadId);
|
|
66
|
+
}
|
|
67
|
+
if (moved.length)
|
|
68
|
+
writeFileAtomic(this.file, JSON.stringify(Object.fromEntries(this.starters)) + "\n", { mode: 0o600 });
|
|
69
|
+
return moved;
|
|
70
|
+
}
|
|
38
71
|
/** Record once; a thread keeps the person it was first opened for. */
|
|
39
72
|
set(threadId, person) {
|
|
40
73
|
if (!person || !KEY.test(person) || !THREAD.test(threadId) || this.starters.has(threadId))
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
// What a generated thread title is asked from.
|
|
2
|
+
//
|
|
3
|
+
// A fresh thread is named from its first message. "Regenerate title" (#1858)
|
|
4
|
+
// names it again from the conversation as it is now: the newest user and bot
|
|
5
|
+
// lines, never tool output, cards or screen frames, and bounded so the
|
|
6
|
+
// one-shot stays as cheap as the first-message one.
|
|
7
|
+
import { redactSecretsInText } from "./redact.js";
|
|
8
|
+
/** The most text either kind of title one-shot ever sees. */
|
|
9
|
+
export const TITLE_INPUT_MAX_CHARS = 1_500;
|
|
10
|
+
const EXCERPT_MAX_MESSAGES = 12;
|
|
11
|
+
const EXCERPT_LINE_MAX_CHARS = 300;
|
|
12
|
+
const ATTACHMENT_TAG = /<attached-(?:image|file)\b[^>]*\/>/g;
|
|
13
|
+
export function threadTitlePrompt(text, source = "first-message") {
|
|
14
|
+
return [
|
|
15
|
+
source === "conversation"
|
|
16
|
+
? "Name the conversation below by what it is about now."
|
|
17
|
+
: "Name the conversation that begins with the message below.",
|
|
18
|
+
"Reply with only a short title: 3 to 6 words, plain text, no quotes, no trailing period.",
|
|
19
|
+
source === "conversation" ? "Conversation:" : "Message:",
|
|
20
|
+
text.trim().slice(0, TITLE_INPUT_MAX_CHARS),
|
|
21
|
+
].join("\n");
|
|
22
|
+
}
|
|
23
|
+
/** The newest user and bot text lines, oldest first, one per line and each
|
|
24
|
+
* clipped, until the next line would pass the input cap. Attachment markup
|
|
25
|
+
* and secrets are scrubbed before anything leaves this machine. Empty when
|
|
26
|
+
* the thread has nothing a title could be drawn from. */
|
|
27
|
+
export function titleConversationExcerpt(messages) {
|
|
28
|
+
const lines = [];
|
|
29
|
+
let length = 0;
|
|
30
|
+
for (let index = messages.length - 1; index >= 0 && lines.length < EXCERPT_MAX_MESSAGES; index--) {
|
|
31
|
+
const message = messages[index];
|
|
32
|
+
// peer asides and still-queued lines are not the conversation yet
|
|
33
|
+
if (message.kind !== "text" || !message.text || message.aside || message.queued)
|
|
34
|
+
continue;
|
|
35
|
+
const text = redactSecretsInText(message.text.replace(ATTACHMENT_TAG, " ")).replace(/\s+/g, " ").trim();
|
|
36
|
+
if (!text)
|
|
37
|
+
continue;
|
|
38
|
+
const clipped = text.length > EXCERPT_LINE_MAX_CHARS ? `${text.slice(0, EXCERPT_LINE_MAX_CHARS - 1)}…` : text;
|
|
39
|
+
const line = `${message.role === "user" ? "User" : "Bot"}: ${clipped}`;
|
|
40
|
+
if (length + line.length + 1 > TITLE_INPUT_MAX_CHARS)
|
|
41
|
+
break;
|
|
42
|
+
lines.push(line);
|
|
43
|
+
length += line.length + 1;
|
|
44
|
+
}
|
|
45
|
+
return lines.reverse().join("\n");
|
|
46
|
+
}
|
|
@@ -1,7 +1,6 @@
|
|
|
1
|
-
// Fish Audio text to speech. The API key stays on the harness: the renderer
|
|
2
|
-
// receives only voice metadata and synthesized audio bytes.
|
|
3
1
|
const API = (process.env.OMB_FISH_AUDIO_API || "https://api.fish.audio").replace(/\/+$/, "");
|
|
4
|
-
|
|
2
|
+
/** Used when no model is saved, so existing setups keep their behavior. */
|
|
3
|
+
export const DEFAULT_FISH_MODEL = "s2.1-pro";
|
|
5
4
|
const MAX_ERROR_LENGTH = 240;
|
|
6
5
|
const PAGE_SIZE = 100;
|
|
7
6
|
const MAX_OWNED_PAGES = 100;
|
|
@@ -124,7 +123,7 @@ export async function listVoices(key) {
|
|
|
124
123
|
});
|
|
125
124
|
return [...new Map(voices.map((voice) => [voice.id, voice])).values()];
|
|
126
125
|
}
|
|
127
|
-
export async function synthesize(text, voiceId, key) {
|
|
126
|
+
export async function synthesize(text, voiceId, key, model = DEFAULT_FISH_MODEL) {
|
|
128
127
|
let res;
|
|
129
128
|
try {
|
|
130
129
|
res = await fetch(`${API}/v1/tts`, {
|
|
@@ -133,7 +132,7 @@ export async function synthesize(text, voiceId, key) {
|
|
|
133
132
|
authorization: `Bearer ${key}`,
|
|
134
133
|
"content-type": "application/json",
|
|
135
134
|
accept: "audio/mpeg",
|
|
136
|
-
model
|
|
135
|
+
model,
|
|
137
136
|
},
|
|
138
137
|
body: JSON.stringify({
|
|
139
138
|
text,
|
|
@@ -69,8 +69,9 @@ export function voiceReady(cfg, voiceId) {
|
|
|
69
69
|
}
|
|
70
70
|
/** What the settings panel needs. Never includes the key — same write-only
|
|
71
71
|
* rule as every other credential. baseUrl and model are Chatterbox
|
|
72
|
-
* settings, not credentials, so they come back in full
|
|
73
|
-
*
|
|
72
|
+
* settings, not credentials, so they come back in full; `fishModel` is the
|
|
73
|
+
* Fish Audio speech model, resolved to its default when unset. `included`
|
|
74
|
+
* says the voice is Cloud Pro's, not a saved key. */
|
|
74
75
|
export function describeVoice(cfg) {
|
|
75
76
|
const provider = voiceProvider(cfg);
|
|
76
77
|
const included = provider === "elevenlabs" && elevenLabs(cfg)?.included === true;
|
|
@@ -81,6 +82,7 @@ export function describeVoice(cfg) {
|
|
|
81
82
|
provider,
|
|
82
83
|
baseUrl: provider === "chatterbox" ? (cfg.tts?.baseUrl ?? "") : "",
|
|
83
84
|
model: provider === "chatterbox" ? (cfg.tts?.model ?? "") : "",
|
|
85
|
+
...(provider === "fish" ? { fishModel: cfg.tts?.fishModel ?? fish.DEFAULT_FISH_MODEL } : {}),
|
|
84
86
|
...(included ? { included: true } : {}),
|
|
85
87
|
};
|
|
86
88
|
}
|
|
@@ -147,7 +149,7 @@ export function speak(cfg, text, voiceId, run) {
|
|
|
147
149
|
const voice = voiceId || cfg.tts?.voice;
|
|
148
150
|
if (!voice)
|
|
149
151
|
throw new NoVoiceConfigured("voice");
|
|
150
|
-
return fish.synthesize(text, voice, key);
|
|
152
|
+
return fish.synthesize(text, voice, key, cfg.tts?.fishModel);
|
|
151
153
|
}
|
|
152
154
|
const credential = elevenLabs(cfg);
|
|
153
155
|
if (!credential)
|