openmausbot 0.1.70 → 0.1.71
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-D1RGhLUK.js → index-CJtyBonr.js} +1 -1
- package/dist/assets/index-CYLdYtYp.css +1 -0
- package/dist/assets/index-Di4ovwu5.js +289 -0
- package/dist/index.html +2 -2
- package/dist-server/container-mcp.js +4 -2
- package/dist-server/drivers/agents-proxy.js +202 -16
- package/dist-server/drivers/pi-mcp-extension.ts +1 -1
- package/dist-server/index.js +2258 -834
- package/dist-server/local-computer.js +2 -1
- package/dist-server/mcp-gate.js +543 -0
- package/dist-server/mcp-server.js +38 -0
- package/dist-server/openmausbot.js +416 -130
- package/dist-server/pair-cli.js +416 -130
- package/dist-server/proxy-paths.js +1 -0
- package/dist-server/server/agent-tool-policy.js +29 -0
- package/dist-server/server/bot-package.js +55 -2
- package/dist-server/server/browser-live.js +12 -7
- package/dist-server/server/config.js +40 -4
- package/dist-server/server/drivers/acp/core.js +2 -0
- package/dist-server/server/drivers/acp/cursor.js +1 -1
- package/dist-server/server/drivers/acp/grok.js +5 -1
- package/dist-server/server/drivers/agents-proxy.js +195 -16
- package/dist-server/server/drivers/boxagent.js +1 -1
- package/dist-server/server/drivers/claude.js +288 -17
- package/dist-server/server/drivers/codex.js +13 -4
- package/dist-server/server/drivers/pi.js +1 -1
- package/dist-server/server/harness/bus.js +1 -1
- package/dist-server/server/index.js +520 -193
- package/dist-server/server/local-computer.js +1 -1
- package/dist-server/server/mcp-gate-config.js +56 -0
- package/dist-server/server/mcp-gate.js +227 -0
- package/dist-server/server/mcp-trim.js +182 -0
- package/dist-server/server/memory-journal.js +440 -0
- package/dist-server/server/memory-store.js +308 -0
- package/dist-server/server/message-db.js +90 -3
- package/dist-server/server/package-export.js +25 -0
- package/dist-server/server/peer-approval.js +1 -1
- package/dist-server/server/proxy-paths.js +1 -0
- package/dist-server/server/redact.js +15 -0
- package/dist-server/server/resume-recovery.js +46 -0
- package/dist-server/server/screen-frame-gate.js +9 -6
- package/dist-server/server/skill-library.js +5 -5
- package/dist-server/server/store.js +7 -3
- package/dist-server/server/system-prompt.js +13 -1
- package/dist-server/server/team-backup.js +35 -0
- package/dist-server/server/tool-summary.js +39 -0
- package/dist-server/server/tts/speech-text.js +3 -1
- package/dist-server/server/turn-context.js +18 -0
- package/dist-server/server/workspace.js +321 -22
- package/dist-server/shared/approval-mode.js +1 -1
- package/dist-server/shared/team-backup.js +13 -2
- package/dist-server/vps-container-mcp.js +4 -2
- package/package.json +1 -1
- package/skills/create-verification-skill/SKILL.md +2 -3
- package/dist/assets/index-B2F__IRS.css +0 -1
- package/dist/assets/index-CLj3t5Ba.js +0 -280
|
@@ -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, maxConcurrentBotThreads, saveConfig, showToolCallsEnabled,
|
|
36
|
+
import { ensureDirs, instanceConfigs, loadConfig, localVmMaxInstances, localVmMode, parseConfigPatch, roomTurnTimeoutMinutes, maxConcurrentBotThreads, saveConfig, showToolCallsEnabled, skillAuthoringEnabled, 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";
|
|
@@ -67,14 +67,16 @@ import { decideRoomPost, emptyRoomPostBudget } from "./room-post-budget.js";
|
|
|
67
67
|
import { isProjectEmoji, mentionedBots, roomResponders, sectionKey, Store, } from "./store.js";
|
|
68
68
|
import * as tts from "./tts/index.js";
|
|
69
69
|
import { narrateTool, toUtterances } from "./tts/speech-text.js";
|
|
70
|
-
import { buildTurnContext, engineIsFresh } from "./turn-context.js";
|
|
70
|
+
import { buildRecoveryText, buildTurnContext, engineIsFresh } from "./turn-context.js";
|
|
71
71
|
import { extractTurnImages } from "./turn-images.js";
|
|
72
72
|
import { TurnWatchdog } from "./turn-watchdog.js";
|
|
73
73
|
import { TurnResources, workspaceResource } from "./turn-resources.js";
|
|
74
|
-
import { ensureWorkspace, ensureTaskWorkspace, updateMemory,
|
|
75
|
-
import {
|
|
74
|
+
import { ensureWorkspace, ensureTaskWorkspace, updateMemory, appendMemoryLog, isMemoryTopicName, memorySystemPrompt, memorySourceLabel, searchMemoryFiles, SESSION_SEARCH_SYSTEM_PROMPT, workspaceDir, } from "./workspace.js";
|
|
75
|
+
import { readMemoryTopic } from "./workspace.js";
|
|
76
|
+
import { MEMORY_INDEX, MemoryStoreError, memoryCapacity, memoryOverview, openMemoryLocation, readMemoryDoc, } from "./memory-store.js";
|
|
77
|
+
import { beginMemoryTurn, endMemoryTurn, flushAllMemoryJournals, flushMemoryJournal, journalMemoryDelete, journalMemoryWrite, readMemoryJournal, revertMemoryChange, } from "./memory-journal.js";
|
|
76
78
|
import { readSectionContext, sectionContextKey, sectionContextLabel, sectionContextSystemPrompt, writeSectionContext, SECTION_CONTEXT_MAX_BYTES, } from "./section-context.js";
|
|
77
|
-
import { applyStagedSkillWrite, getStagedSkillWrite, installSkill, listSkills, listStagedSkillWrites, readSkillFile, rejectStagedSkillWrite, removeSkill, setSkillEnabled, skillsSystemPrompt, stageSkillWrite, } from "./skills.js";
|
|
79
|
+
import { applyStagedSkillWrite, getStagedSkillWrite, installSkill, listSkills, listStagedSkillWrites, isSkillName, readSkillFile, rejectStagedSkillWrite, removeSkill, setSkillEnabled, skillsSystemPrompt, stageSkillWrite, } from "./skills.js";
|
|
78
80
|
import { fetchSkillFromSource } from "./skill-fetch.js";
|
|
79
81
|
import { expandLearnTurnText, learnSource } from "./skill-learn.js";
|
|
80
82
|
import { expandSetupTurnText, setupModeActive, setupSystemPrompt } from "./setup-mode.js";
|
|
@@ -103,7 +105,7 @@ import { flushAllProfileHistory, flushProfileHistory, readHistory, recordProfile
|
|
|
103
105
|
import { fetchBotDirectory, matchDirectoryBots } from "./bot-directory.js";
|
|
104
106
|
import { scoutProject, suggestTeam } from "./project-scout.js";
|
|
105
107
|
import { fetchGithubTeam, fetchLibraryTeam, fetchTeamCatalog } from "./team-library.js";
|
|
106
|
-
import { isBotPackage, packageAgentAsMember, parseBotPackage, renderBotPackageMarkdown } from "./bot-package.js";
|
|
108
|
+
import { BOT_PACKAGE_MAX_SKILLS, isBotPackage, packageAgentAsMember, parseBotPackage, renderBotPackageMarkdown } from "./bot-package.js";
|
|
107
109
|
import { createTeamManifest, importedMemberProfile, parseTeamManifest } from "./team-manifest.js";
|
|
108
110
|
import { readThreadEvents } from "./thread-events.js";
|
|
109
111
|
import { listenWebhookIngress, webhookCredential } from "./webhook-ingress.js";
|
|
@@ -884,6 +886,54 @@ function checkedModelSelection(raw, current, requireAvailableModel = false) {
|
|
|
884
886
|
}
|
|
885
887
|
return { ok: true, selection };
|
|
886
888
|
}
|
|
889
|
+
function checkedExportSkillNames(value, bots) {
|
|
890
|
+
// Sharing instructions is explicit: ordinary package exports include none.
|
|
891
|
+
if (value === undefined)
|
|
892
|
+
return { ok: true, names: [] };
|
|
893
|
+
if (!Array.isArray(value) || value.some((name) => typeof name !== "string" || !isSkillName(name))) {
|
|
894
|
+
return { ok: false, error: "skillIds must be a list of exact imported skill names" };
|
|
895
|
+
}
|
|
896
|
+
const names = [...value];
|
|
897
|
+
if (new Set(names).size !== names.length)
|
|
898
|
+
return { ok: false, error: "skillIds must not contain duplicates" };
|
|
899
|
+
if (names.length > BOT_PACKAGE_MAX_SKILLS)
|
|
900
|
+
return { ok: false, error: `skillIds must contain at most ${BOT_PACKAGE_MAX_SKILLS} names` };
|
|
901
|
+
const available = new Set(bots.flatMap((bot) => listSkills(bot.id).map((skill) => skill.name)));
|
|
902
|
+
const unknown = names.find((name) => !available.has(name));
|
|
903
|
+
if (unknown)
|
|
904
|
+
return { ok: false, error: `skillIds contains unknown imported skill "${unknown}"` };
|
|
905
|
+
return { ok: true, names };
|
|
906
|
+
}
|
|
907
|
+
function collectExportSkills(bots, names) {
|
|
908
|
+
const selected = new Set(names);
|
|
909
|
+
const byBot = new Map();
|
|
910
|
+
if (!selected.size)
|
|
911
|
+
return byBot;
|
|
912
|
+
for (const bot of bots) {
|
|
913
|
+
const assigned = [];
|
|
914
|
+
for (const listing of listSkills(bot.id)) {
|
|
915
|
+
if (!selected.has(listing.name))
|
|
916
|
+
continue;
|
|
917
|
+
const instructions = readSkillFile(bot.id, listing.name);
|
|
918
|
+
if (instructions === null) {
|
|
919
|
+
throw new Error(`Skill "${listing.name}" changed or is unavailable and cannot be exported safely`);
|
|
920
|
+
}
|
|
921
|
+
const skill = {
|
|
922
|
+
name: listing.name,
|
|
923
|
+
description: listing.description,
|
|
924
|
+
...(listing.source ? { source: listing.source } : {}),
|
|
925
|
+
...(listing.license ? { license: listing.license } : {}),
|
|
926
|
+
...(listing.compatibility ? { compatibility: listing.compatibility } : {}),
|
|
927
|
+
instructions,
|
|
928
|
+
};
|
|
929
|
+
// The shared exporter validates duplicate bytes and metadata together.
|
|
930
|
+
assigned.push(skill);
|
|
931
|
+
}
|
|
932
|
+
if (assigned.length)
|
|
933
|
+
byBot.set(bot.id, assigned);
|
|
934
|
+
}
|
|
935
|
+
return byBot;
|
|
936
|
+
}
|
|
887
937
|
function checkedGroupResponder(value, memberIds) {
|
|
888
938
|
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
889
939
|
return null;
|
|
@@ -1010,7 +1060,7 @@ function previewSystemPrompt(bot) {
|
|
|
1010
1060
|
id: "setup",
|
|
1011
1061
|
label: "Setup",
|
|
1012
1062
|
text: setupSystemPrompt(agentsMounted && setupModeActive({ soul: bot.soul, description: bot.description, text: "" }), {
|
|
1013
|
-
skills:
|
|
1063
|
+
skills: skillAuthoringEnabled(cfg),
|
|
1014
1064
|
cwd: bot.cwd,
|
|
1015
1065
|
}),
|
|
1016
1066
|
},
|
|
@@ -1447,6 +1497,165 @@ function groupGoalCoordinatorTurnForEvent(event) {
|
|
|
1447
1497
|
function groupIsWorking(group) {
|
|
1448
1498
|
return Boolean(group.busyBotId) || Boolean(groupTurnOperations.get(group.id)?.size);
|
|
1449
1499
|
}
|
|
1500
|
+
// The public and Chief room tools use the same synchronous validation and write.
|
|
1501
|
+
// Keep authorization at each ingress; no internal caller gains public admin scope.
|
|
1502
|
+
function createChannel(value) {
|
|
1503
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
1504
|
+
throw Object.assign(new Error("channel must be a JSON object"), { status: 400 });
|
|
1505
|
+
}
|
|
1506
|
+
const body = value;
|
|
1507
|
+
const roster = checkedMemberIds(body.memberIds);
|
|
1508
|
+
if (!roster.ok)
|
|
1509
|
+
throw Object.assign(new Error(roster.error), { status: 400 });
|
|
1510
|
+
const { memberIds } = roster;
|
|
1511
|
+
if (body.name !== undefined && typeof body.name !== "string") {
|
|
1512
|
+
throw Object.assign(new Error("channel name must be a string"), { status: 400 });
|
|
1513
|
+
}
|
|
1514
|
+
const name = body.name?.trim() || `${store.bot(memberIds[0]).name} & co.`;
|
|
1515
|
+
if (name.length > 100)
|
|
1516
|
+
throw Object.assign(new Error("channel name must be at most 100 characters"), { status: 400 });
|
|
1517
|
+
let section;
|
|
1518
|
+
if (body.section !== undefined && body.section !== null) {
|
|
1519
|
+
if (typeof body.section !== "string")
|
|
1520
|
+
throw Object.assign(new Error("context must be a string"), { status: 400 });
|
|
1521
|
+
section = body.section.trim() || undefined;
|
|
1522
|
+
if (section && section.length > 60) {
|
|
1523
|
+
throw Object.assign(new Error("context must be at most 60 characters"), { status: 400 });
|
|
1524
|
+
}
|
|
1525
|
+
}
|
|
1526
|
+
let setup;
|
|
1527
|
+
if (body.setup !== undefined) {
|
|
1528
|
+
if (!body.setup || typeof body.setup !== "object" || Array.isArray(body.setup)) {
|
|
1529
|
+
throw Object.assign(new Error("setup must be an object"), { status: 400 });
|
|
1530
|
+
}
|
|
1531
|
+
const requested = body.setup;
|
|
1532
|
+
if (typeof requested.bulletin !== "string") {
|
|
1533
|
+
throw Object.assign(new Error("setup.bulletin must be a string"), { status: 400 });
|
|
1534
|
+
}
|
|
1535
|
+
if (requested.bulletin.length > 12_000) {
|
|
1536
|
+
throw Object.assign(new Error("setup.bulletin must be at most 12000 characters"), { status: 400 });
|
|
1537
|
+
}
|
|
1538
|
+
const responder = checkedGroupResponder(requested.defaultResponder, memberIds);
|
|
1539
|
+
if (!responder)
|
|
1540
|
+
throw Object.assign(new Error("invalid setup.defaultResponder"), { status: 400 });
|
|
1541
|
+
setup = { bulletin: requested.bulletin, defaultResponder: responder, completed: true };
|
|
1542
|
+
}
|
|
1543
|
+
return store.createGroup(name, memberIds, false, section, setup);
|
|
1544
|
+
}
|
|
1545
|
+
function updateChannel(groupId, value) {
|
|
1546
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
1547
|
+
throw Object.assign(new Error("body must be a JSON object"), { status: 400 });
|
|
1548
|
+
}
|
|
1549
|
+
const body = value;
|
|
1550
|
+
const existing = store.group(groupId);
|
|
1551
|
+
if (!existing)
|
|
1552
|
+
throw Object.assign(new Error("no such room"), { status: 404 });
|
|
1553
|
+
if (body.memberIds !== undefined && phoneSecretSubmissions.hasGroup(existing.id)) {
|
|
1554
|
+
throw Object.assign(new Error("this channel is securely saving a credential — try again when it finishes"), { status: 409 });
|
|
1555
|
+
}
|
|
1556
|
+
if (channelTaskBlocked(existing) &&
|
|
1557
|
+
(body.memberIds !== undefined || body.defaultResponder !== undefined || body.bulletin !== undefined)) {
|
|
1558
|
+
throw Object.assign(new Error("this channel is working or waiting on you — finish that turn first"), { status: 409 });
|
|
1559
|
+
}
|
|
1560
|
+
const patch = {};
|
|
1561
|
+
if (body.name !== undefined) {
|
|
1562
|
+
if (typeof body.name !== "string")
|
|
1563
|
+
throw Object.assign(new Error("room name must be a string"), { status: 400 });
|
|
1564
|
+
const name = body.name.trim();
|
|
1565
|
+
if (!name)
|
|
1566
|
+
throw Object.assign(new Error("room name must not be empty"), { status: 400 });
|
|
1567
|
+
if (name.length > 100)
|
|
1568
|
+
throw Object.assign(new Error("room name must be at most 100 characters"), { status: 400 });
|
|
1569
|
+
patch.name = name;
|
|
1570
|
+
}
|
|
1571
|
+
if (body.bulletin !== undefined) {
|
|
1572
|
+
if (typeof body.bulletin !== "string")
|
|
1573
|
+
throw Object.assign(new Error("bulletin must be a string"), { status: 400 });
|
|
1574
|
+
if (body.bulletin.length > 12_000) {
|
|
1575
|
+
throw Object.assign(new Error("bulletin must be at most 12000 characters"), { status: 400 });
|
|
1576
|
+
}
|
|
1577
|
+
patch.bulletin = body.bulletin;
|
|
1578
|
+
}
|
|
1579
|
+
if (body.unread !== undefined) {
|
|
1580
|
+
if (typeof body.unread !== "boolean")
|
|
1581
|
+
throw Object.assign(new Error("unread must be true or false"), { status: 400 });
|
|
1582
|
+
patch.unread = body.unread;
|
|
1583
|
+
}
|
|
1584
|
+
if (body.memberIds !== undefined) {
|
|
1585
|
+
// A DM is the pair it was opened for; only real rooms have a roster.
|
|
1586
|
+
if (existing.dm)
|
|
1587
|
+
throw Object.assign(new Error("direct-message channels cannot change members"), { status: 400 });
|
|
1588
|
+
const roster = checkedMemberIds(body.memberIds);
|
|
1589
|
+
if (!roster.ok)
|
|
1590
|
+
throw Object.assign(new Error(roster.error.replace("channel", "room")), { status: 400 });
|
|
1591
|
+
const removedGoalLead = routines.listRoutines().some((routine) => routine.enabled &&
|
|
1592
|
+
routine.target === "room-goal" &&
|
|
1593
|
+
routine.groupId === existing.id &&
|
|
1594
|
+
!roster.memberIds.includes(routine.botId)) || routines.listRuns().some((run) => run.target === "room-goal" &&
|
|
1595
|
+
run.groupId === existing.id &&
|
|
1596
|
+
["queued", "running", "waiting"].includes(run.status) &&
|
|
1597
|
+
!roster.memberIds.includes(run.botId));
|
|
1598
|
+
if (removedGoalLead) {
|
|
1599
|
+
throw Object.assign(new Error("pause or reassign this room's team-goal routine before removing its lead"), { status: 409 });
|
|
1600
|
+
}
|
|
1601
|
+
patch.memberIds = roster.memberIds;
|
|
1602
|
+
}
|
|
1603
|
+
if (body.defaultResponder !== undefined) {
|
|
1604
|
+
const memberIds = patch.memberIds ?? existing.memberIds;
|
|
1605
|
+
const responder = checkedGroupResponder(body.defaultResponder, memberIds);
|
|
1606
|
+
if (!responder)
|
|
1607
|
+
throw Object.assign(new Error("invalid default responder"), { status: 400 });
|
|
1608
|
+
patch.defaultResponder = responder;
|
|
1609
|
+
}
|
|
1610
|
+
if (body.cwd !== undefined) {
|
|
1611
|
+
if (existing.dm)
|
|
1612
|
+
throw Object.assign(new Error("direct-message channels cannot have a working folder"), { status: 400 });
|
|
1613
|
+
if (existing.pinnedCwd !== undefined) {
|
|
1614
|
+
throw Object.assign(new Error("the room's working folder is fixed after its first turn"), { status: 409 });
|
|
1615
|
+
}
|
|
1616
|
+
const checked = validateBotCwd(body.cwd);
|
|
1617
|
+
if (!checked.ok)
|
|
1618
|
+
throw Object.assign(new Error(checked.error), { status: 400 });
|
|
1619
|
+
patch.cwd = checked.cwd ?? undefined;
|
|
1620
|
+
}
|
|
1621
|
+
// one pinned message per room; null/"" clears. The id is not
|
|
1622
|
+
// validated against the transcript here — a pin whose message was
|
|
1623
|
+
// edited away or deleted simply resolves to nothing in the UI.
|
|
1624
|
+
if (body.pinnedMessageId !== undefined) {
|
|
1625
|
+
if (body.pinnedMessageId === null || body.pinnedMessageId === "")
|
|
1626
|
+
patch.pinnedMessageId = undefined;
|
|
1627
|
+
else if (typeof body.pinnedMessageId === "string" && /^[\w-]+$/.test(body.pinnedMessageId)) {
|
|
1628
|
+
patch.pinnedMessageId = body.pinnedMessageId;
|
|
1629
|
+
}
|
|
1630
|
+
else
|
|
1631
|
+
throw Object.assign(new Error("pinnedMessageId must be a message id"), { status: 400 });
|
|
1632
|
+
}
|
|
1633
|
+
// same contract as a bot's sidebar section: null/"" clears, 60 chars max
|
|
1634
|
+
if (body.section !== undefined) {
|
|
1635
|
+
if (body.section === null)
|
|
1636
|
+
patch.section = undefined;
|
|
1637
|
+
else if (typeof body.section !== "string")
|
|
1638
|
+
throw Object.assign(new Error("section must be a string"), { status: 400 });
|
|
1639
|
+
else {
|
|
1640
|
+
const trimmed = body.section.trim();
|
|
1641
|
+
if (!trimmed)
|
|
1642
|
+
patch.section = undefined;
|
|
1643
|
+
else if (trimmed.length > 60)
|
|
1644
|
+
throw Object.assign(new Error("section must be at most 60 characters"), { status: 400 });
|
|
1645
|
+
else
|
|
1646
|
+
patch.section = trimmed;
|
|
1647
|
+
}
|
|
1648
|
+
}
|
|
1649
|
+
const group = store.patchGroup(groupId, patch);
|
|
1650
|
+
if (!group)
|
|
1651
|
+
throw Object.assign(new Error("no such room"), { status: 404 });
|
|
1652
|
+
return group;
|
|
1653
|
+
}
|
|
1654
|
+
const channelTaskBlocked = (group) => groupIsWorking(group) ||
|
|
1655
|
+
store.groupTasks(group.id).some((task) => store.messagesFor(task.threadId).some((message) => message.kind === "options" &&
|
|
1656
|
+
message.card?.requestId &&
|
|
1657
|
+
!message.card.answered &&
|
|
1658
|
+
!message.card.dismissed));
|
|
1450
1659
|
function publicGroupState(group) {
|
|
1451
1660
|
return { ...group, working: groupIsWorking(group) };
|
|
1452
1661
|
}
|
|
@@ -1899,7 +2108,7 @@ function broadcast(payload) {
|
|
|
1899
2108
|
replayBuffer.push({ seq, kind, frame: kind === "screen" ? null : frame, clientFrame: kind === "screen" ? null : clientFrame });
|
|
1900
2109
|
if (replayBuffer.length > REPLAY_MAX)
|
|
1901
2110
|
replayBuffer.shift();
|
|
1902
|
-
for (const client of
|
|
2111
|
+
for (const client of Array.from(sseClients)) {
|
|
1903
2112
|
if (!wants(client, kind))
|
|
1904
2113
|
continue;
|
|
1905
2114
|
// Screen frames are replaceable and durable events are not: see
|
|
@@ -2205,6 +2414,18 @@ bus.subscribe((event) => {
|
|
|
2205
2414
|
else if (event.type !== "session.exited")
|
|
2206
2415
|
watchdog.touch(event.threadId);
|
|
2207
2416
|
});
|
|
2417
|
+
// Memory journal turn boundary (server/memory-journal.ts). A bot's own
|
|
2418
|
+
// file-tool writes to MEMORY.md and memory/ have no hook to tap, so the
|
|
2419
|
+
// diff against the baseline taken at dispatch is made when the turn
|
|
2420
|
+
// settles — session.exited too, because a turn that died may still have
|
|
2421
|
+
// written. Fire-and-forget by construction: endMemoryTurn swallows its own
|
|
2422
|
+
// failures and never reaches the fold below.
|
|
2423
|
+
bus.subscribe((event) => {
|
|
2424
|
+
if (shouldIgnoreProviderEvent(event))
|
|
2425
|
+
return;
|
|
2426
|
+
if (event.type === "turn.completed" || event.type === "session.exited")
|
|
2427
|
+
endMemoryTurn(event.threadId);
|
|
2428
|
+
});
|
|
2208
2429
|
// Bots currently working with nobody at the keyboard — a webhook turn, or a
|
|
2209
2430
|
// turn a webhook-driven bot handed to a teammate. Auto mode is a decision
|
|
2210
2431
|
// someone made for turns they were present for, so these don't inherit it:
|
|
@@ -2616,7 +2837,7 @@ bus.subscribe((event) => {
|
|
|
2616
2837
|
const existing = store.messagesFor(event.threadId).find((m) => m.id === messageId)?.tool;
|
|
2617
2838
|
toolName = existing?.name ?? "tool";
|
|
2618
2839
|
store.patchMessage(event.threadId, messageId, {
|
|
2619
|
-
tool: { name: toolName, ok: event.ok, spoken: existing?.spoken },
|
|
2840
|
+
tool: { name: toolName, ok: event.ok, spoken: existing?.spoken, summary: existing?.summary },
|
|
2620
2841
|
});
|
|
2621
2842
|
toolMessageByItem.delete(itemKey);
|
|
2622
2843
|
}
|
|
@@ -2648,7 +2869,7 @@ bus.subscribe((event) => {
|
|
|
2648
2869
|
const message = pushMessage({
|
|
2649
2870
|
role: "bot",
|
|
2650
2871
|
kind: "activity",
|
|
2651
|
-
tool: { name, spoken: narrateTool(name) ?? undefined },
|
|
2872
|
+
tool: { name, spoken: narrateTool(name) ?? undefined, summary: event.summary },
|
|
2652
2873
|
});
|
|
2653
2874
|
if (event.itemId)
|
|
2654
2875
|
toolMessageByItem.set(`${event.threadId}:${event.itemId}`, message.id);
|
|
@@ -3880,7 +4101,7 @@ async function startTurn(botId, text, opts) {
|
|
|
3880
4101
|
// that never mounts agent tools (or a turn already at the comms-depth cap)
|
|
3881
4102
|
// must not be steered into — or told about — tools it cannot call.
|
|
3882
4103
|
const agentsMounted = commsDepth < MAX_COMMS_DEPTH && instance.adapter.capabilities.agentsMcp === true;
|
|
3883
|
-
const skillAuthoring =
|
|
4104
|
+
const skillAuthoring = skillAuthoringEnabled(cfg) && agentsMounted;
|
|
3884
4105
|
// Setup mode's turn-text rewrite (parseSetupCommand/expandSetupTurnText)
|
|
3885
4106
|
// must not run ahead of a system prompt that can't explain it: a driver
|
|
3886
4107
|
// without agent tools sees the user's literal "/setup ..." message. The
|
|
@@ -3901,6 +4122,10 @@ async function startTurn(botId, text, opts) {
|
|
|
3901
4122
|
// this already-built turn must either keep its old session or replay on the
|
|
3902
4123
|
// following turn, never start a blank session with no transcript.
|
|
3903
4124
|
const resumeCursor = resume ? task.resumeCursors[instanceId] : undefined;
|
|
4125
|
+
// A cursor the provider no longer honours must not brick the thread: the
|
|
4126
|
+
// driver may fall back to ONE fresh session, and this is what it sends
|
|
4127
|
+
// there, so the new session is not blank (server/resume-recovery.ts).
|
|
4128
|
+
const recoveryText = resumeCursor !== undefined ? buildRecoveryText({ text: turnText, transcript }) : undefined;
|
|
3904
4129
|
const persona = [
|
|
3905
4130
|
`You are ${bot.name}, a personal bot in OpenMausBot.`,
|
|
3906
4131
|
bot.title && `Role: ${bot.title}.`,
|
|
@@ -3968,8 +4193,11 @@ async function startTurn(botId, text, opts) {
|
|
|
3968
4193
|
// desk, not the whole house — and the workspace is where its
|
|
3969
4194
|
// MEMORY.md lives. API/box engines have no local filesystem story.
|
|
3970
4195
|
const worksInWorkspace = instance.driverKind !== "grok" && instance.driverKind !== "boxAgent";
|
|
3971
|
-
if (worksInWorkspace)
|
|
4196
|
+
if (worksInWorkspace) {
|
|
3972
4197
|
ensureWorkspace(bot.id);
|
|
4198
|
+
// baseline for the journal's turn-boundary diff (see the bus hook)
|
|
4199
|
+
beginMemoryTurn(bot.id, threadId);
|
|
4200
|
+
}
|
|
3973
4201
|
const privateWorkspace = worksInWorkspace ? ensureTaskWorkspace(bot.id, threadId) : undefined;
|
|
3974
4202
|
const skillInstructions = renderSkillInstructions(selectedSkills, {
|
|
3975
4203
|
includeRoot: worksInWorkspace && opts?.runOn !== "cloud",
|
|
@@ -4343,8 +4571,11 @@ async function startTurn(botId, text, opts) {
|
|
|
4343
4571
|
// the active task's own session — another task's cursor would
|
|
4344
4572
|
// resume the wrong conversation and defeat the context bubble
|
|
4345
4573
|
resumeCursor,
|
|
4574
|
+
...(recoveryText !== undefined ? { recoveryText } : {}),
|
|
4346
4575
|
transcript,
|
|
4347
4576
|
system: prompt.text,
|
|
4577
|
+
systemStable: prompt.stable,
|
|
4578
|
+
systemVolatile: prompt.volatile,
|
|
4348
4579
|
integrations,
|
|
4349
4580
|
cwd,
|
|
4350
4581
|
}), () => !directTurnClaimExists(bot.id, dispatchClaimId, threadId), async () => {
|
|
@@ -5141,7 +5372,7 @@ setupRetry = 0) {
|
|
|
5141
5372
|
};
|
|
5142
5373
|
try {
|
|
5143
5374
|
const integrations = {};
|
|
5144
|
-
const skillAuthoring =
|
|
5375
|
+
const skillAuthoring = skillAuthoringEnabled(cfg) &&
|
|
5145
5376
|
hop === 0 &&
|
|
5146
5377
|
!skillAuthoringClaim.claimed &&
|
|
5147
5378
|
!cardContinuation &&
|
|
@@ -5382,6 +5613,9 @@ setupRetry = 0) {
|
|
|
5382
5613
|
// conversation, not a different bot
|
|
5383
5614
|
const worksInWorkspace = instance.driverKind !== "grok" && instance.driverKind !== "boxAgent";
|
|
5384
5615
|
const workspace = worksInWorkspace ? ensureWorkspace(bot.id) : undefined;
|
|
5616
|
+
// a room member's memory writes are journaled the same as a 1:1 turn's
|
|
5617
|
+
if (workspace)
|
|
5618
|
+
beginMemoryTurn(bot.id, threadId);
|
|
5385
5619
|
// The room's folder pins here — on the first turn that actually
|
|
5386
5620
|
// dispatches, not at PATCH time — so a folder set on a never-used room
|
|
5387
5621
|
// still takes effect, while a room that already worked somewhere never
|
|
@@ -5401,12 +5635,14 @@ setupRetry = 0) {
|
|
|
5401
5635
|
{ id: "section-context", label: "Section context", text: sectionContextSystemPrompt(bot.section) },
|
|
5402
5636
|
// the room path has always put a newline before memory and trimmed
|
|
5403
5637
|
// the block's leading space; keep that so existing prompts are
|
|
5404
|
-
// byte-identical
|
|
5405
|
-
|
|
5638
|
+
// byte-identical. The write guidance follows the tools actually
|
|
5639
|
+
// mounted, exactly as the 1:1 path decides it: memory_update is on the
|
|
5640
|
+
// agents server, so a room turn with it must be told to use it too.
|
|
5641
|
+
{ id: "memory", label: "Memory", text: workspace ? `\n${memorySystemPrompt(bot.id, { managedWrites: Boolean(integrations.agents) }).trim()}` : "" },
|
|
5406
5642
|
{ id: "skills", label: "Skills index", text: workspace ? skillsSystemPrompt(bot.id) : "" },
|
|
5407
5643
|
{ id: "skill-instructions", label: "Skill instructions", text: renderSkillInstructions(selectedSkills, { includeRoot: Boolean(workspace) }) },
|
|
5408
5644
|
{ id: "playbooks", label: "Playbooks", text: installedPlaybookInstructions(text, bot.playbooks) },
|
|
5409
|
-
])
|
|
5645
|
+
]);
|
|
5410
5646
|
// run the turn and wait for it to settle, folding the reply text so a
|
|
5411
5647
|
// chained @mention can be routed afterwards
|
|
5412
5648
|
// Claim only after setup succeeded. An unavailable, busy, unsupported, or
|
|
@@ -5509,7 +5745,9 @@ setupRetry = 0) {
|
|
|
5509
5745
|
text,
|
|
5510
5746
|
images: turnImages,
|
|
5511
5747
|
approvalMode: approvalModeForTurn(readyBot, false, threadId),
|
|
5512
|
-
system: roomSystem,
|
|
5748
|
+
system: roomSystem.text,
|
|
5749
|
+
systemStable: roomSystem.stable,
|
|
5750
|
+
systemVolatile: roomSystem.volatile,
|
|
5513
5751
|
cwd,
|
|
5514
5752
|
integrations,
|
|
5515
5753
|
...memberTurnSelection(readyBot.modelSelection),
|
|
@@ -7044,7 +7282,7 @@ function configStatus() {
|
|
|
7044
7282
|
maxInstances: localVmMaxInstances(cfg),
|
|
7045
7283
|
},
|
|
7046
7284
|
features: {
|
|
7047
|
-
|
|
7285
|
+
skillAuthoring: skillAuthoringEnabled(cfg),
|
|
7048
7286
|
showToolCalls: showToolCallsEnabled(cfg),
|
|
7049
7287
|
browser: builtInBrowserEnabled(cfg),
|
|
7050
7288
|
},
|
|
@@ -7235,6 +7473,26 @@ function json(res, status, body) {
|
|
|
7235
7473
|
res.writeHead(status, { "content-type": "application/json" });
|
|
7236
7474
|
res.end(data);
|
|
7237
7475
|
}
|
|
7476
|
+
/** A store refusal is a client error with a status of its own (400 path,
|
|
7477
|
+
* 409 conflict, 413 too large); a 409 also carries what is on disk now so
|
|
7478
|
+
* the editor can show the bot's version instead of guessing. Anything else
|
|
7479
|
+
* is a real failure and goes to the handler's catch-all. */
|
|
7480
|
+
function replyMemoryError(res, error) {
|
|
7481
|
+
if (!(error instanceof MemoryStoreError))
|
|
7482
|
+
throw error;
|
|
7483
|
+
if (error.code === "conflict") {
|
|
7484
|
+
return json(res, error.status, { error: error.message, code: error.code, currentHash: error.currentHash, current: error.current });
|
|
7485
|
+
}
|
|
7486
|
+
return json(res, error.status, { error: error.message, code: error.code });
|
|
7487
|
+
}
|
|
7488
|
+
/** The journal row as the panel shows it: the full prior text stays on
|
|
7489
|
+
* the server (a revert needs it there, the list does not), and the thread
|
|
7490
|
+
* id becomes the chat title people recognise. */
|
|
7491
|
+
function journalEntryForClient(botId, entry) {
|
|
7492
|
+
const { before: _before, ...visible } = entry;
|
|
7493
|
+
const threadTitle = entry.threadId ? store.taskByThread(botId, entry.threadId)?.title : undefined;
|
|
7494
|
+
return threadTitle ? { ...visible, threadTitle } : visible;
|
|
7495
|
+
}
|
|
7238
7496
|
function readBody(req, limit = 1_000_000) {
|
|
7239
7497
|
return new Promise((resolve, reject) => {
|
|
7240
7498
|
let data = "";
|
|
@@ -7602,10 +7860,23 @@ const handleRequest = async (req, res) => {
|
|
|
7602
7860
|
throw Object.assign(new Error("the internal turn capability has expired"), { status: 401 });
|
|
7603
7861
|
}
|
|
7604
7862
|
};
|
|
7863
|
+
// Where an entry came from, as the person will read it in MEMORY.md:
|
|
7864
|
+
// the room or the thread title, never a bare id unless nothing else
|
|
7865
|
+
// names the conversation.
|
|
7866
|
+
const memorySource = () => memorySourceLabel({
|
|
7867
|
+
room: store.groupByThread(internalCapability.threadId),
|
|
7868
|
+
task: store.taskByThread(internalSender.id, internalCapability.threadId),
|
|
7869
|
+
threadId: internalCapability.threadId,
|
|
7870
|
+
});
|
|
7605
7871
|
if (method === "POST" && path === "/api/internal/memory") {
|
|
7606
7872
|
const body = await readInternalBody();
|
|
7607
|
-
const result = updateMemory(internalSender.id, { action: body.action, text: body.text, oldText: body.oldText });
|
|
7608
|
-
return json(res, result.ok ? 200 : result.code === "conflict" ? 409 : result.code === "
|
|
7873
|
+
const result = updateMemory(internalSender.id, { action: body.action, text: body.text, oldText: body.oldText }, { source: memorySource() });
|
|
7874
|
+
return json(res, result.ok ? 200 : result.code === "conflict" ? 409 : result.code === "over-budget" ? 413 : 400, result);
|
|
7875
|
+
}
|
|
7876
|
+
if (method === "POST" && path === "/api/internal/memory/log") {
|
|
7877
|
+
const body = await readInternalBody();
|
|
7878
|
+
const result = appendMemoryLog(internalSender.id, body.text, { source: memorySource() });
|
|
7879
|
+
return json(res, result.ok ? 200 : 400, result);
|
|
7609
7880
|
}
|
|
7610
7881
|
if (method === "POST" && path === "/api/internal/browser/mcp") {
|
|
7611
7882
|
const body = await readInternalBody();
|
|
@@ -7910,6 +8181,16 @@ const handleRequest = async (req, res) => {
|
|
|
7910
8181
|
return json(res, 400, { error: "q is required" });
|
|
7911
8182
|
const rawLimit = Number(url.searchParams.get("limit"));
|
|
7912
8183
|
const limit = Number.isFinite(rawLimit) && rawLimit > 0 ? Math.min(Math.trunc(rawLimit), 25) : 12;
|
|
8184
|
+
// Memory files ride along by default: the bot's own notes are as
|
|
8185
|
+
// much its notebook as its transcripts, and scoped the same way —
|
|
8186
|
+
// the caller's own bot, never another's.
|
|
8187
|
+
const scope = url.searchParams.get("scope") ?? "all";
|
|
8188
|
+
if (scope !== "all" && scope !== "conversations" && scope !== "memory") {
|
|
8189
|
+
return json(res, 400, { error: "scope must be all, conversations, or memory" });
|
|
8190
|
+
}
|
|
8191
|
+
const memoryHits = scope === "conversations" ? [] : searchMemoryFiles(from.id, q, limit);
|
|
8192
|
+
if (scope === "memory")
|
|
8193
|
+
return json(res, 200, { hits: [], memoryHits });
|
|
7913
8194
|
const ownThreads = [...new Set([from.threadId, ...(from.tasks ?? []).map((task) => task.threadId)])];
|
|
7914
8195
|
// A room is the only place a recall can be a disclosure: in a 1:1 the
|
|
7915
8196
|
// user already owns every thread the bot can reach.
|
|
@@ -7923,7 +8204,7 @@ const handleRequest = async (req, res) => {
|
|
|
7923
8204
|
if (inRoom) {
|
|
7924
8205
|
discloseRecall(from, fromThreadId, hits.filter((hit) => hit.crossed).map((hit) => hit.threadId));
|
|
7925
8206
|
}
|
|
7926
|
-
return json(res, 200, { hits });
|
|
8207
|
+
return json(res, 200, { hits, memoryHits });
|
|
7927
8208
|
}
|
|
7928
8209
|
// session_read: the whole message behind a session_search hit. Same
|
|
7929
8210
|
// own-bot scope — a message id from another bot's thread reads as
|
|
@@ -7957,8 +8238,8 @@ const handleRequest = async (req, res) => {
|
|
|
7957
8238
|
});
|
|
7958
8239
|
}
|
|
7959
8240
|
if (method === "GET" && path === "/api/internal/skills") {
|
|
7960
|
-
if (!
|
|
7961
|
-
return json(res, 403, { error: "
|
|
8241
|
+
if (!skillAuthoringEnabled(cfg))
|
|
8242
|
+
return json(res, 403, { error: "skill authoring is not enabled in Settings" });
|
|
7962
8243
|
if (!internalCapability.skillAuthoring) {
|
|
7963
8244
|
return json(res, 403, { error: "skill authoring is not enabled for this turn" });
|
|
7964
8245
|
}
|
|
@@ -7973,8 +8254,8 @@ const handleRequest = async (req, res) => {
|
|
|
7973
8254
|
});
|
|
7974
8255
|
}
|
|
7975
8256
|
if (method === "POST" && path === "/api/internal/skills/stage") {
|
|
7976
|
-
if (!
|
|
7977
|
-
return json(res, 403, { error: "
|
|
8257
|
+
if (!skillAuthoringEnabled(cfg))
|
|
8258
|
+
return json(res, 403, { error: "skill authoring is not enabled in Settings" });
|
|
7978
8259
|
if (!internalCapability.skillAuthoring) {
|
|
7979
8260
|
return json(res, 403, { error: "skill authoring is not enabled for this turn" });
|
|
7980
8261
|
}
|
|
@@ -8611,6 +8892,86 @@ const handleRequest = async (req, res) => {
|
|
|
8611
8892
|
model: safeBot.modelSelection.model,
|
|
8612
8893
|
});
|
|
8613
8894
|
}
|
|
8895
|
+
if (method === "POST" && (path === "/api/internal/create-room" || path === "/api/internal/manage-room")) {
|
|
8896
|
+
const body = await readInternalBody();
|
|
8897
|
+
const chief = store.bot(internalCapability.botId);
|
|
8898
|
+
if (chief.hidden || !chief.chiefOfStaff || !connectorThread(chief.id, internalCapability.threadId)) {
|
|
8899
|
+
return json(res, 403, { error: "only an active section Chief of Staff can manage rooms" });
|
|
8900
|
+
}
|
|
8901
|
+
if (!body || typeof body !== "object" || Array.isArray(body)) {
|
|
8902
|
+
return json(res, 400, { error: "room request must be a JSON object" });
|
|
8903
|
+
}
|
|
8904
|
+
// ponytail: no new room-administration approval flow. Fail closed for
|
|
8905
|
+
// chiefs whose peer changes need review; add a proposal card if needed.
|
|
8906
|
+
if (chief.approvePeerComms) {
|
|
8907
|
+
return json(res, 403, { error: "peer approval is required; ask the user to make this room change" });
|
|
8908
|
+
}
|
|
8909
|
+
// Section labels are a permission boundary, not bot-owned organization.
|
|
8910
|
+
// A Chief may not recruit excluded peers or acquire a foreign transcript.
|
|
8911
|
+
const allowedIds = new Set([chief.id, ...reachablePeers(store.bots, chief).map((bot) => bot.id)]);
|
|
8912
|
+
const allowedRoster = (ids) => ids.includes(chief.id) && ids.every((id) => allowedIds.has(id));
|
|
8913
|
+
if (body.section !== undefined && (typeof body.section !== "string" || sectionKey(body.section) !== sectionKey(chief.section))) {
|
|
8914
|
+
return json(res, 403, { error: "rooms must stay in your own section; ask the user to move them" });
|
|
8915
|
+
}
|
|
8916
|
+
if (path === "/api/internal/create-room") {
|
|
8917
|
+
if ((internalCapability.createdRooms ?? 0) >= 4) {
|
|
8918
|
+
return json(res, 429, { error: "you can create at most 4 rooms in one turn" });
|
|
8919
|
+
}
|
|
8920
|
+
const parsed = z.object({
|
|
8921
|
+
fromBotId: z.string().optional(), fromThreadId: z.string().optional(),
|
|
8922
|
+
name: z.string().trim().min(1).max(100), memberIds: z.array(z.string()).min(1).max(100),
|
|
8923
|
+
section: z.string().optional(), bulletin: z.string().max(12_000).optional(),
|
|
8924
|
+
}).strict().safeParse(body);
|
|
8925
|
+
if (!parsed.success)
|
|
8926
|
+
return json(res, 400, { error: "provide a room name, memberIds, and optional bulletin (at most 12000 characters)" });
|
|
8927
|
+
const memberIds = [...new Set([chief.id, ...parsed.data.memberIds])];
|
|
8928
|
+
if (!allowedRoster(memberIds)) {
|
|
8929
|
+
return json(res, 403, { error: "include yourself and only active, allowed peers from your section" });
|
|
8930
|
+
}
|
|
8931
|
+
const group = createChannel({
|
|
8932
|
+
name: redactSecretsInText(parsed.data.name), memberIds, section: chief.section,
|
|
8933
|
+
setup: { bulletin: redactSecretsInText(parsed.data.bulletin ?? ""), defaultResponder: { kind: "member", botId: chief.id } },
|
|
8934
|
+
});
|
|
8935
|
+
internalCapability.createdRooms = (internalCapability.createdRooms ?? 0) + 1;
|
|
8936
|
+
return json(res, 201, { id: group.id, name: group.name, section: group.section || "General", memberIds: group.memberIds, memberCount: group.memberIds.length });
|
|
8937
|
+
}
|
|
8938
|
+
const parsed = z.object({
|
|
8939
|
+
fromBotId: z.string().optional(), fromThreadId: z.string().optional(),
|
|
8940
|
+
roomId: z.string(), action: z.enum(["add_members", "remove_members", "set_members", "rename", "set_bulletin"]),
|
|
8941
|
+
memberIds: z.array(z.string()).min(1).max(100).optional(),
|
|
8942
|
+
name: z.string().trim().min(1).max(100).optional(), bulletin: z.string().max(12_000).optional(),
|
|
8943
|
+
}).strict().safeParse(body);
|
|
8944
|
+
if (!parsed.success)
|
|
8945
|
+
return json(res, 400, { error: "provide roomId and a supported room action; section moves are user-only" });
|
|
8946
|
+
const room = store.group(parsed.data.roomId);
|
|
8947
|
+
if (!room || room.dm || sectionKey(room.section) !== sectionKey(chief.section) || !allowedRoster(room.memberIds)) {
|
|
8948
|
+
return json(res, 403, { error: "you can only manage rooms you belong to with allowed peers in your own section" });
|
|
8949
|
+
}
|
|
8950
|
+
const { action, memberIds, name, bulletin } = parsed.data;
|
|
8951
|
+
const patch = {};
|
|
8952
|
+
if (action === "rename") {
|
|
8953
|
+
if (name === undefined)
|
|
8954
|
+
return json(res, 400, { error: "rename requires a name" });
|
|
8955
|
+
patch.name = redactSecretsInText(name);
|
|
8956
|
+
}
|
|
8957
|
+
else if (action === "set_bulletin") {
|
|
8958
|
+
if (bulletin === undefined)
|
|
8959
|
+
return json(res, 400, { error: "set_bulletin requires bulletin text; use an empty string to clear it" });
|
|
8960
|
+
patch.bulletin = redactSecretsInText(bulletin);
|
|
8961
|
+
}
|
|
8962
|
+
else {
|
|
8963
|
+
if (!memberIds || memberIds.some((id) => !allowedIds.has(id))) {
|
|
8964
|
+
return json(res, 403, { error: "memberIds must name only yourself or active, allowed peers in your section" });
|
|
8965
|
+
}
|
|
8966
|
+
const next = action === "add_members" ? [...new Set([...room.memberIds, ...memberIds])]
|
|
8967
|
+
: action === "remove_members" ? room.memberIds.filter((id) => !memberIds.includes(id)) : memberIds;
|
|
8968
|
+
if (!allowedRoster(next))
|
|
8969
|
+
return json(res, 403, { error: "keep yourself in the room; only the user can remove its managing Chief" });
|
|
8970
|
+
patch.memberIds = next;
|
|
8971
|
+
}
|
|
8972
|
+
const updated = updateChannel(room.id, patch);
|
|
8973
|
+
return json(res, 200, { ok: true, memberIds: updated.memberIds, memberCount: updated.memberIds.length, message: `Updated room “${updated.name}”.` });
|
|
8974
|
+
}
|
|
8614
8975
|
if (method === "POST" && path === "/api/internal/request-credential") {
|
|
8615
8976
|
const body = await readInternalBody();
|
|
8616
8977
|
const from = internalSender;
|
|
@@ -9447,47 +9808,7 @@ const handleRequest = async (req, res) => {
|
|
|
9447
9808
|
}
|
|
9448
9809
|
// ── channels (persisted internally as groups) ───────────────────────
|
|
9449
9810
|
if (method === "POST" && path === "/api/groups") {
|
|
9450
|
-
const
|
|
9451
|
-
if (!body || typeof body !== "object" || Array.isArray(body)) {
|
|
9452
|
-
return json(res, 400, { error: "channel must be a JSON object" });
|
|
9453
|
-
}
|
|
9454
|
-
const roster = checkedMemberIds(body.memberIds);
|
|
9455
|
-
if (!roster.ok)
|
|
9456
|
-
return json(res, 400, { error: roster.error });
|
|
9457
|
-
const { memberIds } = roster;
|
|
9458
|
-
if (body.name !== undefined && typeof body.name !== "string") {
|
|
9459
|
-
return json(res, 400, { error: "channel name must be a string" });
|
|
9460
|
-
}
|
|
9461
|
-
const name = body.name?.trim() || `${store.bot(memberIds[0]).name} & co.`;
|
|
9462
|
-
if (name.length > 100)
|
|
9463
|
-
return json(res, 400, { error: "channel name must be at most 100 characters" });
|
|
9464
|
-
let section;
|
|
9465
|
-
if (body.section !== undefined && body.section !== null) {
|
|
9466
|
-
if (typeof body.section !== "string")
|
|
9467
|
-
return json(res, 400, { error: "context must be a string" });
|
|
9468
|
-
section = body.section.trim() || undefined;
|
|
9469
|
-
if (section && section.length > 60) {
|
|
9470
|
-
return json(res, 400, { error: "context must be at most 60 characters" });
|
|
9471
|
-
}
|
|
9472
|
-
}
|
|
9473
|
-
let setup;
|
|
9474
|
-
if (body.setup !== undefined) {
|
|
9475
|
-
if (!body.setup || typeof body.setup !== "object" || Array.isArray(body.setup)) {
|
|
9476
|
-
return json(res, 400, { error: "setup must be an object" });
|
|
9477
|
-
}
|
|
9478
|
-
const requested = body.setup;
|
|
9479
|
-
if (typeof requested.bulletin !== "string") {
|
|
9480
|
-
return json(res, 400, { error: "setup.bulletin must be a string" });
|
|
9481
|
-
}
|
|
9482
|
-
if (requested.bulletin.length > 12_000) {
|
|
9483
|
-
return json(res, 400, { error: "setup.bulletin must be at most 12000 characters" });
|
|
9484
|
-
}
|
|
9485
|
-
const responder = checkedGroupResponder(requested.defaultResponder, memberIds);
|
|
9486
|
-
if (!responder)
|
|
9487
|
-
return json(res, 400, { error: "invalid setup.defaultResponder" });
|
|
9488
|
-
setup = { bulletin: requested.bulletin, defaultResponder: responder, completed: true };
|
|
9489
|
-
}
|
|
9490
|
-
const group = store.createGroup(name, memberIds, false, section, setup);
|
|
9811
|
+
const group = createChannel(await readBody(req));
|
|
9491
9812
|
return json(res, 201, { group: { ...publicGroupState(group), messages: [] } });
|
|
9492
9813
|
}
|
|
9493
9814
|
if (method === "POST" && path === "/api/teams/export") {
|
|
@@ -9506,12 +9827,17 @@ const handleRequest = async (req, res) => {
|
|
|
9506
9827
|
return json(res, 200, createTeamBackup(store, routines.listRoutines(), name));
|
|
9507
9828
|
}
|
|
9508
9829
|
if (body.format === "package") {
|
|
9830
|
+
const selectedBots = store.bots.filter((bot) => !bot.hidden);
|
|
9831
|
+
const skillNames = checkedExportSkillNames(body.skillIds, selectedBots);
|
|
9832
|
+
if (!skillNames.ok)
|
|
9833
|
+
return json(res, 400, { error: skillNames.error });
|
|
9509
9834
|
const document = createBotPackageExport({
|
|
9510
9835
|
name,
|
|
9511
9836
|
authorName: profileName,
|
|
9512
|
-
bots:
|
|
9837
|
+
bots: selectedBots,
|
|
9513
9838
|
groups: store.groups,
|
|
9514
9839
|
routines: routines.listRoutines(),
|
|
9840
|
+
skillsByBot: collectExportSkills(selectedBots, skillNames.names),
|
|
9515
9841
|
});
|
|
9516
9842
|
return json(res, 200, {
|
|
9517
9843
|
name: document.package.name,
|
|
@@ -9656,8 +9982,8 @@ const handleRequest = async (req, res) => {
|
|
|
9656
9982
|
const pkg = packageDocument?.package;
|
|
9657
9983
|
const importName = pkg?.name ?? manifest.team.name;
|
|
9658
9984
|
const sourceMembers = pkg
|
|
9659
|
-
? pkg.agents.map((agent) => ({ member: packageAgentAsMember(agent), playbookKeys: agent.playbooks ?? [] }))
|
|
9660
|
-
: manifest.team.members.map((member) => ({ member, playbookKeys: [] }));
|
|
9985
|
+
? pkg.agents.map((agent) => ({ member: packageAgentAsMember(agent), playbookKeys: agent.playbooks ?? [], skillNames: agent.skills ?? [] }))
|
|
9986
|
+
: manifest.team.members.map((member) => ({ member, playbookKeys: [], skillNames: [] }));
|
|
9661
9987
|
const importedBots = [];
|
|
9662
9988
|
const createdGroups = [];
|
|
9663
9989
|
const createdRoutineIds = [];
|
|
@@ -9680,6 +10006,7 @@ const handleRequest = async (req, res) => {
|
|
|
9680
10006
|
}
|
|
9681
10007
|
}
|
|
9682
10008
|
const playbookByKey = new Map((pkg?.playbooks ?? []).map((playbook) => [playbook.key, playbook]));
|
|
10009
|
+
const packageSkillByName = new Map((pkg?.skills?.entries ?? []).map((skill) => [skill.name, skill]));
|
|
9683
10010
|
for (const source of sourceMembers) {
|
|
9684
10011
|
const member = source.member;
|
|
9685
10012
|
// importedMemberProfile is the authority boundary: persona fields
|
|
@@ -9713,6 +10040,17 @@ const handleRequest = async (req, res) => {
|
|
|
9713
10040
|
}
|
|
9714
10041
|
: {}),
|
|
9715
10042
|
});
|
|
10043
|
+
for (const skillName of source.skillNames) {
|
|
10044
|
+
const skill = packageSkillByName.get(skillName);
|
|
10045
|
+
if (!skill)
|
|
10046
|
+
throw new Error(`Package skill "${skillName}" is unavailable`);
|
|
10047
|
+
const installed = installSkill(created.id, skill.source ?? `package:${pkg.id}`, [
|
|
10048
|
+
{ path: "SKILL.md", content: skill.instructions },
|
|
10049
|
+
]);
|
|
10050
|
+
if ("error" in installed) {
|
|
10051
|
+
throw new Error(`Package skill "${skillName}" could not be imported: ${installed.error}`);
|
|
10052
|
+
}
|
|
10053
|
+
}
|
|
9716
10054
|
memberIds.set(member.key, created.id);
|
|
9717
10055
|
}
|
|
9718
10056
|
// A package is an explicit structure import: its rooms are created
|
|
@@ -9835,11 +10173,6 @@ const handleRequest = async (req, res) => {
|
|
|
9835
10173
|
return json(res, 200, { group: publicGroupState(updated) });
|
|
9836
10174
|
}
|
|
9837
10175
|
// ── channel tasks: separate conversations for the same team ────────
|
|
9838
|
-
const channelTaskBlocked = (group) => groupIsWorking(group) ||
|
|
9839
|
-
store.groupTasks(group.id).some((task) => store.messagesFor(task.threadId).some((message) => message.kind === "options" &&
|
|
9840
|
-
message.card?.requestId &&
|
|
9841
|
-
!message.card.answered &&
|
|
9842
|
-
!message.card.dismissed));
|
|
9843
10176
|
// A scheduled goal starts in a detached task. Let the user open the
|
|
9844
10177
|
// exact task that owns the live operation (or a durable approval card)
|
|
9845
10178
|
// so they can observe or unblock it; switching to an unrelated task is
|
|
@@ -9955,113 +10288,7 @@ const handleRequest = async (req, res) => {
|
|
|
9955
10288
|
if (field)
|
|
9956
10289
|
return json(res, 403, { error: `forbidden: this session may rename or mark a room, not change "${field}" (needs the admin scope)` });
|
|
9957
10290
|
}
|
|
9958
|
-
|
|
9959
|
-
return json(res, 400, { error: "body must be a JSON object" });
|
|
9960
|
-
}
|
|
9961
|
-
const existing = store.group(m[1]);
|
|
9962
|
-
if (!existing)
|
|
9963
|
-
return json(res, 404, { error: "no such room" });
|
|
9964
|
-
if (body.memberIds !== undefined && phoneSecretSubmissions.hasGroup(existing.id)) {
|
|
9965
|
-
return json(res, 409, { error: "this channel is securely saving a credential — try again when it finishes" });
|
|
9966
|
-
}
|
|
9967
|
-
if (channelTaskBlocked(existing) &&
|
|
9968
|
-
(body.memberIds !== undefined || body.defaultResponder !== undefined || body.bulletin !== undefined)) {
|
|
9969
|
-
return json(res, 409, { error: "this channel is working or waiting on you — finish that turn first" });
|
|
9970
|
-
}
|
|
9971
|
-
const patch = {};
|
|
9972
|
-
if (body.name !== undefined) {
|
|
9973
|
-
if (typeof body.name !== "string")
|
|
9974
|
-
return json(res, 400, { error: "room name must be a string" });
|
|
9975
|
-
const name = body.name.trim();
|
|
9976
|
-
if (!name)
|
|
9977
|
-
return json(res, 400, { error: "room name must not be empty" });
|
|
9978
|
-
if (name.length > 100)
|
|
9979
|
-
return json(res, 400, { error: "room name must be at most 100 characters" });
|
|
9980
|
-
patch.name = name;
|
|
9981
|
-
}
|
|
9982
|
-
if (body.bulletin !== undefined) {
|
|
9983
|
-
if (typeof body.bulletin !== "string")
|
|
9984
|
-
return json(res, 400, { error: "bulletin must be a string" });
|
|
9985
|
-
if (body.bulletin.length > 12_000) {
|
|
9986
|
-
return json(res, 400, { error: "bulletin must be at most 12000 characters" });
|
|
9987
|
-
}
|
|
9988
|
-
patch.bulletin = body.bulletin;
|
|
9989
|
-
}
|
|
9990
|
-
if (body.unread !== undefined) {
|
|
9991
|
-
if (typeof body.unread !== "boolean")
|
|
9992
|
-
return json(res, 400, { error: "unread must be true or false" });
|
|
9993
|
-
patch.unread = body.unread;
|
|
9994
|
-
}
|
|
9995
|
-
if (body.memberIds !== undefined) {
|
|
9996
|
-
// A DM is the pair it was opened for; only real rooms have a roster.
|
|
9997
|
-
if (existing.dm)
|
|
9998
|
-
return json(res, 400, { error: "direct-message channels cannot change members" });
|
|
9999
|
-
const roster = checkedMemberIds(body.memberIds);
|
|
10000
|
-
if (!roster.ok)
|
|
10001
|
-
return json(res, 400, { error: roster.error.replace("channel", "room") });
|
|
10002
|
-
const removedGoalLead = routines.listRoutines().some((routine) => routine.enabled &&
|
|
10003
|
-
routine.target === "room-goal" &&
|
|
10004
|
-
routine.groupId === existing.id &&
|
|
10005
|
-
!roster.memberIds.includes(routine.botId)) || routines.listRuns().some((run) => run.target === "room-goal" &&
|
|
10006
|
-
run.groupId === existing.id &&
|
|
10007
|
-
["queued", "running", "waiting"].includes(run.status) &&
|
|
10008
|
-
!roster.memberIds.includes(run.botId));
|
|
10009
|
-
if (removedGoalLead) {
|
|
10010
|
-
return json(res, 409, {
|
|
10011
|
-
error: "pause or reassign this room's team-goal routine before removing its lead",
|
|
10012
|
-
});
|
|
10013
|
-
}
|
|
10014
|
-
patch.memberIds = roster.memberIds;
|
|
10015
|
-
}
|
|
10016
|
-
if (body.defaultResponder !== undefined) {
|
|
10017
|
-
const memberIds = patch.memberIds ?? existing.memberIds;
|
|
10018
|
-
const responder = checkedGroupResponder(body.defaultResponder, memberIds);
|
|
10019
|
-
if (!responder)
|
|
10020
|
-
return json(res, 400, { error: "invalid default responder" });
|
|
10021
|
-
patch.defaultResponder = responder;
|
|
10022
|
-
}
|
|
10023
|
-
if (body.cwd !== undefined) {
|
|
10024
|
-
if (existing.dm)
|
|
10025
|
-
return json(res, 400, { error: "direct-message channels cannot have a working folder" });
|
|
10026
|
-
if (existing.pinnedCwd !== undefined) {
|
|
10027
|
-
return json(res, 409, { error: "the room's working folder is fixed after its first turn" });
|
|
10028
|
-
}
|
|
10029
|
-
const checked = validateBotCwd(body.cwd);
|
|
10030
|
-
if (!checked.ok)
|
|
10031
|
-
return json(res, 400, { error: checked.error });
|
|
10032
|
-
patch.cwd = checked.cwd ?? undefined;
|
|
10033
|
-
}
|
|
10034
|
-
// one pinned message per room; null/"" clears. The id is not
|
|
10035
|
-
// validated against the transcript here — a pin whose message was
|
|
10036
|
-
// edited away or deleted simply resolves to nothing in the UI.
|
|
10037
|
-
if (body.pinnedMessageId !== undefined) {
|
|
10038
|
-
if (body.pinnedMessageId === null || body.pinnedMessageId === "")
|
|
10039
|
-
patch.pinnedMessageId = undefined;
|
|
10040
|
-
else if (typeof body.pinnedMessageId === "string" && /^[\w-]+$/.test(body.pinnedMessageId)) {
|
|
10041
|
-
patch.pinnedMessageId = body.pinnedMessageId;
|
|
10042
|
-
}
|
|
10043
|
-
else
|
|
10044
|
-
return json(res, 400, { error: "pinnedMessageId must be a message id" });
|
|
10045
|
-
}
|
|
10046
|
-
// same contract as a bot's sidebar section: null/"" clears, 60 chars max
|
|
10047
|
-
if (body.section !== undefined) {
|
|
10048
|
-
if (body.section === null)
|
|
10049
|
-
patch.section = undefined;
|
|
10050
|
-
else if (typeof body.section !== "string")
|
|
10051
|
-
return json(res, 400, { error: "section must be a string" });
|
|
10052
|
-
else {
|
|
10053
|
-
const trimmed = body.section.trim();
|
|
10054
|
-
if (!trimmed)
|
|
10055
|
-
patch.section = undefined;
|
|
10056
|
-
else if (trimmed.length > 60)
|
|
10057
|
-
return json(res, 400, { error: "section must be at most 60 characters" });
|
|
10058
|
-
else
|
|
10059
|
-
patch.section = trimmed;
|
|
10060
|
-
}
|
|
10061
|
-
}
|
|
10062
|
-
const group = store.patchGroup(m[1], patch);
|
|
10063
|
-
if (!group)
|
|
10064
|
-
return json(res, 404, { error: "no such room" });
|
|
10291
|
+
const group = updateChannel(m[1], body);
|
|
10065
10292
|
return json(res, 200, { group: publicGroupState(group) });
|
|
10066
10293
|
}
|
|
10067
10294
|
m = path.match(/^\/api\/groups\/([\w-]+)\/read$/);
|
|
@@ -11339,31 +11566,130 @@ const handleRequest = async (req, res) => {
|
|
|
11339
11566
|
broadcast({ kind: "bot", bot: visible });
|
|
11340
11567
|
return json(res, 200, { bot: visible });
|
|
11341
11568
|
}
|
|
11342
|
-
// ── bot memory: MEMORY.md + memory/ topic files
|
|
11343
|
-
// The files already belong to the
|
|
11344
|
-
// workspace)
|
|
11345
|
-
//
|
|
11346
|
-
//
|
|
11569
|
+
// ── bot memory: MEMORY.md + memory/ topic and log files ──────────────
|
|
11570
|
+
// The files already belong to the person (plain markdown in the bot's
|
|
11571
|
+
// workspace). server/memory-store.ts decides which paths can be reached
|
|
11572
|
+
// and refuses a save whose expectedHash no longer matches the file;
|
|
11573
|
+
// server/memory-journal.ts records every change made here so it can be
|
|
11574
|
+
// read back and reverted. Reads never create the workspace — a bot that
|
|
11575
|
+
// has not run yet simply has nothing to show. Admin scope by default
|
|
11576
|
+
// (request-auth.ts), like the profile routes above.
|
|
11347
11577
|
m = path.match(/^\/api\/bots\/([\w-]+)\/memory$/);
|
|
11348
11578
|
if (m && method === "GET") {
|
|
11349
11579
|
if (!store.bot(m[1]))
|
|
11350
11580
|
return json(res, 404, { error: "no such bot" });
|
|
11351
|
-
|
|
11581
|
+
try {
|
|
11582
|
+
const overview = memoryOverview(m[1]);
|
|
11583
|
+
// `text` and `truncated` ride along one release for clients of the
|
|
11584
|
+
// old whole-file shape; the panel reads the file through /memory/file
|
|
11585
|
+
return json(res, 200, { ...overview, text: readMemoryDoc(m[1], MEMORY_INDEX).text, truncated: overview.index.truncated });
|
|
11586
|
+
}
|
|
11587
|
+
catch (error) {
|
|
11588
|
+
return replyMemoryError(res, error);
|
|
11589
|
+
}
|
|
11352
11590
|
}
|
|
11353
11591
|
if (m && method === "PUT") {
|
|
11592
|
+
// The pre-panel whole-file write, kept one release: no hash check, so
|
|
11593
|
+
// it can still overwrite a note the bot just wrote — journaled as the
|
|
11594
|
+
// person's so at least the journal can undo it.
|
|
11354
11595
|
if (!store.bot(m[1]))
|
|
11355
11596
|
return json(res, 404, { error: "no such bot" });
|
|
11356
11597
|
const parsed = z.object({ text: z.string() }).safeParse(await readBody(req));
|
|
11357
11598
|
if (!parsed.success)
|
|
11358
11599
|
return json(res, 400, { error: "text must be a string" });
|
|
11359
|
-
|
|
11360
|
-
|
|
11361
|
-
|
|
11600
|
+
try {
|
|
11601
|
+
const { doc } = journalMemoryWrite(m[1], MEMORY_INDEX, parsed.data.text, { actor: "person", via: "api" });
|
|
11602
|
+
return json(res, 200, { ok: true, hash: doc.hash, truncated: memoryCapacity(doc.text).truncated });
|
|
11603
|
+
}
|
|
11604
|
+
catch (error) {
|
|
11605
|
+
// the old route answered 400 for an oversized body; keep that for
|
|
11606
|
+
// its callers while the new route says 413
|
|
11607
|
+
if (error instanceof MemoryStoreError && error.code === "too-large")
|
|
11608
|
+
return json(res, 400, { error: error.message });
|
|
11609
|
+
return replyMemoryError(res, error);
|
|
11610
|
+
}
|
|
11611
|
+
}
|
|
11612
|
+
m = path.match(/^\/api\/bots\/([\w-]+)\/memory\/file$/);
|
|
11613
|
+
if (m && method === "GET") {
|
|
11614
|
+
if (!store.bot(m[1]))
|
|
11615
|
+
return json(res, 404, { error: "no such bot" });
|
|
11616
|
+
try {
|
|
11617
|
+
return json(res, 200, readMemoryDoc(m[1], url.searchParams.get("path") ?? MEMORY_INDEX));
|
|
11618
|
+
}
|
|
11619
|
+
catch (error) {
|
|
11620
|
+
return replyMemoryError(res, error);
|
|
11621
|
+
}
|
|
11622
|
+
}
|
|
11623
|
+
if (m && method === "PUT") {
|
|
11624
|
+
if (!store.bot(m[1]))
|
|
11625
|
+
return json(res, 404, { error: "no such bot" });
|
|
11626
|
+
const parsed = z
|
|
11627
|
+
.object({ path: z.string().default(MEMORY_INDEX), text: z.string(), expectedHash: z.string().optional() })
|
|
11628
|
+
.safeParse(await readBody(req));
|
|
11629
|
+
if (!parsed.success)
|
|
11630
|
+
return json(res, 400, { error: "text must be a string; path and expectedHash are optional strings" });
|
|
11631
|
+
try {
|
|
11632
|
+
const { doc, entry } = journalMemoryWrite(m[1], parsed.data.path, parsed.data.text, {
|
|
11633
|
+
actor: "person",
|
|
11634
|
+
via: "ui",
|
|
11635
|
+
expectedHash: parsed.data.expectedHash,
|
|
11362
11636
|
});
|
|
11637
|
+
return json(res, 200, { ok: true, ...doc, entry: entry ? journalEntryForClient(m[1], entry) : null, overview: memoryOverview(m[1]) });
|
|
11638
|
+
}
|
|
11639
|
+
catch (error) {
|
|
11640
|
+
return replyMemoryError(res, error);
|
|
11641
|
+
}
|
|
11642
|
+
}
|
|
11643
|
+
if (m && method === "DELETE") {
|
|
11644
|
+
if (!store.bot(m[1]))
|
|
11645
|
+
return json(res, 404, { error: "no such bot" });
|
|
11646
|
+
const file = url.searchParams.get("path") ?? "";
|
|
11647
|
+
try {
|
|
11648
|
+
const entry = journalMemoryDelete(m[1], file, { actor: "person", via: "ui" });
|
|
11649
|
+
return json(res, 200, { ok: true, path: file, entry: entry ? journalEntryForClient(m[1], entry) : null, overview: memoryOverview(m[1]) });
|
|
11363
11650
|
}
|
|
11364
|
-
|
|
11365
|
-
|
|
11366
|
-
|
|
11651
|
+
catch (error) {
|
|
11652
|
+
return replyMemoryError(res, error);
|
|
11653
|
+
}
|
|
11654
|
+
}
|
|
11655
|
+
m = path.match(/^\/api\/bots\/([\w-]+)\/memory\/journal$/);
|
|
11656
|
+
if (m && method === "GET") {
|
|
11657
|
+
if (!store.bot(m[1]))
|
|
11658
|
+
return json(res, 404, { error: "no such bot" });
|
|
11659
|
+
// the row a save queued a moment ago may not have reached disk yet
|
|
11660
|
+
await flushMemoryJournal(m[1]);
|
|
11661
|
+
const limit = Math.min(200, Math.max(1, Number(url.searchParams.get("limit") ?? 50) || 50));
|
|
11662
|
+
const botId = m[1];
|
|
11663
|
+
return json(res, 200, { entries: readMemoryJournal(botId, limit).map((entry) => journalEntryForClient(botId, entry)) });
|
|
11664
|
+
}
|
|
11665
|
+
m = path.match(/^\/api\/bots\/([\w-]+)\/memory\/journal\/([\w-]+)\/revert$/);
|
|
11666
|
+
if (m && method === "POST") {
|
|
11667
|
+
if (!store.bot(m[1]))
|
|
11668
|
+
return json(res, 404, { error: "no such bot" });
|
|
11669
|
+
await flushMemoryJournal(m[1]);
|
|
11670
|
+
const result = revertMemoryChange(m[1], m[2]);
|
|
11671
|
+
if (!result.ok)
|
|
11672
|
+
return json(res, result.status, { error: result.error });
|
|
11673
|
+
return json(res, 200, { ok: true, ...result.doc, entry: result.entry ? journalEntryForClient(m[1], result.entry) : null, overview: memoryOverview(m[1]) });
|
|
11674
|
+
}
|
|
11675
|
+
m = path.match(/^\/api\/bots\/([\w-]+)\/memory\/open$/);
|
|
11676
|
+
if (m && method === "POST") {
|
|
11677
|
+
if (!store.bot(m[1]))
|
|
11678
|
+
return json(res, 404, { error: "no such bot" });
|
|
11679
|
+
const parsed = z.object({ target: z.enum(["obsidian", "folder"]) }).safeParse(await readBody(req));
|
|
11680
|
+
if (!parsed.success)
|
|
11681
|
+
return json(res, 400, { error: "target must be obsidian or folder" });
|
|
11682
|
+
// The folder is on this machine's disk; opening it only makes sense
|
|
11683
|
+
// from this machine. A paired phone or a remote browser gets the path
|
|
11684
|
+
// to open by hand instead.
|
|
11685
|
+
const workspacePath = memoryOverview(m[1]).workspacePath;
|
|
11686
|
+
if (auth.kind !== "loopback") {
|
|
11687
|
+
return json(res, 403, { error: `This only works on the computer running OpenMausBot. The memory folder there is ${workspacePath}`, workspacePath });
|
|
11688
|
+
}
|
|
11689
|
+
const opened = await openMemoryLocation(m[1], parsed.data.target);
|
|
11690
|
+
if (!opened.ok)
|
|
11691
|
+
return json(res, 500, { error: opened.error, workspacePath: opened.workspacePath });
|
|
11692
|
+
return json(res, 200, opened);
|
|
11367
11693
|
}
|
|
11368
11694
|
m = path.match(/^\/api\/bots\/([\w-]+)\/memory\/topics\/([^/]+)$/);
|
|
11369
11695
|
if (m && method === "GET") {
|
|
@@ -13560,6 +13886,7 @@ const gracefulShutdown = createGracefulShutdown({
|
|
|
13560
13886
|
await browserRuntime.closeAll();
|
|
13561
13887
|
},
|
|
13562
13888
|
() => flushAllProfileHistory(),
|
|
13889
|
+
() => flushAllMemoryJournals(),
|
|
13563
13890
|
],
|
|
13564
13891
|
// Cleanup jobs run concurrently. Release only after they settle (or reach
|
|
13565
13892
|
// the shutdown deadline), immediately before the process exits, so no new
|