openmausbot 0.1.85 → 0.1.87
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/README.md +2 -2
- package/dist/assets/KaTeX_AMS-Regular-BQhdFMY1.woff2 +0 -0
- package/dist/assets/KaTeX_AMS-Regular-DMm9YOAa.woff +0 -0
- package/dist/assets/KaTeX_AMS-Regular-DRggAlZN.ttf +0 -0
- package/dist/assets/KaTeX_Caligraphic-Bold-ATXxdsX0.ttf +0 -0
- package/dist/assets/KaTeX_Caligraphic-Bold-BEiXGLvX.woff +0 -0
- package/dist/assets/KaTeX_Caligraphic-Bold-Dq_IR9rO.woff2 +0 -0
- package/dist/assets/KaTeX_Caligraphic-Regular-CTRA-rTL.woff +0 -0
- package/dist/assets/KaTeX_Caligraphic-Regular-Di6jR-x-.woff2 +0 -0
- package/dist/assets/KaTeX_Caligraphic-Regular-wX97UBjC.ttf +0 -0
- package/dist/assets/KaTeX_Fraktur-Bold-BdnERNNW.ttf +0 -0
- package/dist/assets/KaTeX_Fraktur-Bold-BsDP51OF.woff +0 -0
- package/dist/assets/KaTeX_Fraktur-Bold-CL6g_b3V.woff2 +0 -0
- package/dist/assets/KaTeX_Fraktur-Regular-CB_wures.ttf +0 -0
- package/dist/assets/KaTeX_Fraktur-Regular-CTYiF6lA.woff2 +0 -0
- package/dist/assets/KaTeX_Fraktur-Regular-Dxdc4cR9.woff +0 -0
- package/dist/assets/KaTeX_Main-Bold-Cx986IdX.woff2 +0 -0
- package/dist/assets/KaTeX_Main-Bold-Jm3AIy58.woff +0 -0
- package/dist/assets/KaTeX_Main-Bold-waoOVXN0.ttf +0 -0
- package/dist/assets/KaTeX_Main-BoldItalic-DxDJ3AOS.woff2 +0 -0
- package/dist/assets/KaTeX_Main-BoldItalic-DzxPMmG6.ttf +0 -0
- package/dist/assets/KaTeX_Main-BoldItalic-SpSLRI95.woff +0 -0
- package/dist/assets/KaTeX_Main-Italic-3WenGoN9.ttf +0 -0
- package/dist/assets/KaTeX_Main-Italic-BMLOBm91.woff +0 -0
- package/dist/assets/KaTeX_Main-Italic-NWA7e6Wa.woff2 +0 -0
- package/dist/assets/KaTeX_Main-Regular-B22Nviop.woff2 +0 -0
- package/dist/assets/KaTeX_Main-Regular-Dr94JaBh.woff +0 -0
- package/dist/assets/KaTeX_Main-Regular-ypZvNtVU.ttf +0 -0
- package/dist/assets/KaTeX_Math-BoldItalic-B3XSjfu4.ttf +0 -0
- package/dist/assets/KaTeX_Math-BoldItalic-CZnvNsCZ.woff2 +0 -0
- package/dist/assets/KaTeX_Math-BoldItalic-iY-2wyZ7.woff +0 -0
- package/dist/assets/KaTeX_Math-Italic-DA0__PXp.woff +0 -0
- package/dist/assets/KaTeX_Math-Italic-flOr_0UB.ttf +0 -0
- package/dist/assets/KaTeX_Math-Italic-t53AETM-.woff2 +0 -0
- package/dist/assets/KaTeX_SansSerif-Bold-CFMepnvq.ttf +0 -0
- package/dist/assets/KaTeX_SansSerif-Bold-D1sUS0GD.woff2 +0 -0
- package/dist/assets/KaTeX_SansSerif-Bold-DbIhKOiC.woff +0 -0
- package/dist/assets/KaTeX_SansSerif-Italic-C3H0VqGB.woff2 +0 -0
- package/dist/assets/KaTeX_SansSerif-Italic-DN2j7dab.woff +0 -0
- package/dist/assets/KaTeX_SansSerif-Italic-YYjJ1zSn.ttf +0 -0
- package/dist/assets/KaTeX_SansSerif-Regular-BNo7hRIc.ttf +0 -0
- package/dist/assets/KaTeX_SansSerif-Regular-CS6fqUqJ.woff +0 -0
- package/dist/assets/KaTeX_SansSerif-Regular-DDBCnlJ7.woff2 +0 -0
- package/dist/assets/KaTeX_Script-Regular-C5JkGWo-.ttf +0 -0
- package/dist/assets/KaTeX_Script-Regular-D3wIWfF6.woff2 +0 -0
- package/dist/assets/KaTeX_Script-Regular-D5yQViql.woff +0 -0
- package/dist/assets/KaTeX_Size1-Regular-C195tn64.woff +0 -0
- package/dist/assets/KaTeX_Size1-Regular-Dbsnue_I.ttf +0 -0
- package/dist/assets/KaTeX_Size1-Regular-mCD8mA8B.woff2 +0 -0
- package/dist/assets/KaTeX_Size2-Regular-B7gKUWhC.ttf +0 -0
- package/dist/assets/KaTeX_Size2-Regular-Dy4dx90m.woff2 +0 -0
- package/dist/assets/KaTeX_Size2-Regular-oD1tc_U0.woff +0 -0
- package/dist/assets/KaTeX_Size3-Regular-CTq5MqoE.woff +0 -0
- package/dist/assets/KaTeX_Size3-Regular-DgpXs0kz.ttf +0 -0
- package/dist/assets/KaTeX_Size4-Regular-BF-4gkZK.woff +0 -0
- package/dist/assets/KaTeX_Size4-Regular-DWFBv043.ttf +0 -0
- package/dist/assets/KaTeX_Size4-Regular-Dl5lxZxV.woff2 +0 -0
- package/dist/assets/KaTeX_Typewriter-Regular-C0xS9mPB.woff +0 -0
- package/dist/assets/KaTeX_Typewriter-Regular-CO6r4hn1.woff2 +0 -0
- package/dist/assets/KaTeX_Typewriter-Regular-D3Ib7_Hf.ttf +0 -0
- package/dist/assets/abnfDiagram-O67JEVCF-BTiuL79F.js +1 -0
- package/dist/assets/arc-C88Blc2v.js +1 -0
- package/dist/assets/architectureDiagram-NJMV4G6O-BZsJIZrb.js +36 -0
- package/dist/assets/blockDiagram-BEXU5L5S-CVeMe9Ah.js +140 -0
- package/dist/assets/c4Diagram-YGBWAQC7-Cedk3Bhx.js +38 -0
- package/dist/assets/channel-taCNRlUU.js +1 -0
- package/dist/assets/chunk-2Q5K7J3B-D-VRqzTB.js +1 -0
- package/dist/assets/chunk-3FUC2YCW-BChov01Y.js +2 -0
- package/dist/assets/chunk-5VM5RSS4-C-TfwvHy.js +15 -0
- package/dist/assets/chunk-GTNCS2PH-Db7CGj5Y.js +88 -0
- package/dist/assets/chunk-GWA4HPMP-CNvIfqKT.js +1 -0
- package/dist/assets/chunk-JWPE2WC7-C_bEp5tq.js +1 -0
- package/dist/assets/chunk-NETBCI7D-Coi4BOls.js +1 -0
- package/dist/assets/chunk-WEXAMYUT-CzylEuL3.js +1 -0
- package/dist/assets/chunk-XXDRQBXY-m7p7ztaD.js +1 -0
- package/dist/assets/classDiagram-v2-NBCMYWYE-BMy6o9x2.js +217 -0
- package/dist/assets/cose-bilkent-JH36ORCC-BDQ__0_J.js +1 -0
- package/dist/assets/cynefin-OW5HDTMX-Djv4ghUb.js +166 -0
- package/dist/assets/cynefinDiagram-VND7K2PF-BULrlGYH.js +62 -0
- package/dist/assets/cytoscape.esm-BB4DxJjf.js +321 -0
- package/dist/assets/dagre-6A5THRUB-D-jrT_hU.js +4 -0
- package/dist/assets/defaultLocale-DX6XiGOO.js +1 -0
- package/dist/assets/diagram-22UHCM2B-p2UhQmfL.js +200 -0
- package/dist/assets/diagram-3UASUU5V-DHvGWZ7q.js +24 -0
- package/dist/assets/diagram-ATOU4E4O-ChhKDot-.js +3 -0
- package/dist/assets/diagram-CDSNMT55-B-T7A43R.js +30 -0
- package/dist/assets/diagram-MLGK6HIB-CJm8ENNY.js +24 -0
- package/dist/assets/diagram-MPIPVDR6-510tlza8.js +41 -0
- package/dist/assets/ebnfDiagram-ZINNZB2B-PkvRPHel.js +1 -0
- package/dist/assets/elk-276RUBZZ-BVBVPoX3.js +27 -0
- package/dist/assets/erDiagram-OPXOYQCR-BgYMVm7L.js +99 -0
- package/dist/assets/flowDiagram-KWPJA3E3-BGqsnc_3.js +213 -0
- package/dist/assets/ganttDiagram-FUAMR5RP-D_-UeReH.js +292 -0
- package/dist/assets/gitGraphDiagram-X574FWY7-D0uooONx.js +106 -0
- package/dist/assets/graph-D8UmR1vK.js +1 -0
- package/dist/assets/index-BwdMAQ6p.css +1 -0
- package/dist/assets/index-ByJennr7.js +581 -0
- package/dist/assets/{index-CTNNoqSJ.js → index-zRsJERGJ.js} +1 -1
- package/dist/assets/infoDiagram-VRGFBTTK-B9e1z77f.js +2 -0
- package/dist/assets/init-Gi6I4Gst.js +1 -0
- package/dist/assets/ishikawaDiagram-OU5B5YK6-BB4vNwvD.js +70 -0
- package/dist/assets/journeyDiagram-ZHPQQLJL-DpkosOTB.js +139 -0
- package/dist/assets/kanban-definition-PNTS6WVX-DbDP4kUu.js +89 -0
- package/dist/assets/linear-DBJGPmjT.js +1 -0
- package/dist/assets/mermaid.core-DWPRi-ue.js +311 -0
- package/dist/assets/mindmap-definition-NLK3R4M7-nszBbJmx.js +96 -0
- package/dist/assets/ordinal-Cboi1Yqb.js +1 -0
- package/dist/assets/pegDiagram-GJSIUBJH-CWykJWu_.js +1 -0
- package/dist/assets/pieDiagram-5QR66LMP-g3Da-Vnv.js +39 -0
- package/dist/assets/quadrantDiagram-O4NWA36T-Wzrlpzgh.js +7 -0
- package/dist/assets/railroadDiagram-XR7U4H2S-moHTFMC0.js +1 -0
- package/dist/assets/requirementDiagram-PLB6GJNP-CwfXdFH8.js +84 -0
- package/dist/assets/sankeyDiagram-IPEJSGJF-3ZROYvnq.js +40 -0
- package/dist/assets/sequenceDiagram-PO4LG4MO-E1TBebP8.js +169 -0
- package/dist/assets/sizeCapture-INFHLROL-DHKEU5pu.js +1 -0
- package/dist/assets/stateDiagram-v2-GCMORJYK-BguEj09D.js +291 -0
- package/dist/assets/swimlanes-2SLR337P-C7IDYnhd.js +1 -0
- package/dist/assets/swimlanesDiagram-TC7HE7FX-BpEdoB2e.js +8 -0
- package/dist/assets/timeline-definition-EJHVYXUP-DCW-F5PW.js +120 -0
- package/dist/assets/usecaseDiagram-POWQR4AR-xBMFtWYV.js +370 -0
- package/dist/assets/vennDiagram-UO4OBE2U-B5nIkuH8.js +34 -0
- package/dist/assets/wardleyDiagram-VNRHLVJA-DYFAhdbr.js +78 -0
- package/dist/assets/xychartDiagram-PMCCYNJV-DeXwX6Cz.js +7 -0
- package/dist/index.html +2 -2
- package/dist-server/browser-proxy.js +2 -1
- package/dist-server/container-mcp.js +1047 -173
- package/dist-server/drivers/agents-proxy.js +16121 -1504
- package/dist-server/drivers/phone-proxy.js +33 -2
- package/dist-server/enterprise/LICENSE +32 -0
- package/dist-server/enterprise/server/index.js +5 -5
- package/dist-server/index.js +16134 -7446
- package/dist-server/local-computer-proxy.js +3 -3
- package/dist-server/local-computer.js +53 -17
- package/dist-server/mcp-server.js +3 -1
- package/dist-server/openmausbot.js +9353 -6124
- package/dist-server/pair-cli.js +9353 -6124
- package/dist-server/permission-proxy.js +29 -9
- package/dist-server/server/admin-activity.js +410 -0
- package/dist-server/server/attachments.js +55 -0
- package/dist-server/server/auto-approve.js +4 -3
- package/dist-server/server/auto-vm-claims.js +1 -0
- package/dist-server/server/bot-package.js +18 -292
- package/dist-server/server/bot-profile.js +5 -17
- package/dist-server/server/bot-visibility.js +462 -0
- package/dist-server/server/box-create-idempotency.js +3 -3
- package/dist-server/server/box.js +59 -56
- package/dist-server/server/browser-proxy.js +3 -2
- package/dist-server/server/browser-runtime.js +38 -2
- package/dist-server/server/channel-queue.js +2 -0
- package/dist-server/server/cli-prompts.js +24 -2
- package/dist-server/server/cli.js +81 -12
- package/dist-server/server/command-allowlist.js +154 -0
- package/dist-server/server/composio.js +49 -11
- package/dist-server/server/computer-wait.js +22 -9
- package/dist-server/server/config.js +133 -17
- package/dist-server/server/connector-verdict.js +141 -0
- package/dist-server/server/container-computer.js +1 -1
- package/dist-server/server/decision-log.js +209 -48
- package/dist-server/server/default-model-selection.js +29 -3
- package/dist-server/server/delegations.js +97 -44
- package/dist-server/server/drivers/acp/core.js +163 -33
- package/dist-server/server/drivers/acp/grok.js +4 -3
- package/dist-server/server/drivers/agents-call.js +1162 -0
- package/dist-server/server/drivers/agents-catalog.js +689 -0
- package/dist-server/server/drivers/agents-client.js +28 -0
- package/dist-server/server/drivers/agents-proxy.js +18 -1722
- package/dist-server/server/drivers/boxagent.js +174 -36
- package/dist-server/server/drivers/builtIn.js +2 -0
- package/dist-server/server/drivers/chat-box-tools.js +90 -0
- package/dist-server/server/drivers/chat-images.js +54 -0
- package/dist-server/server/drivers/chat-mcp-tools.js +55 -10
- package/dist-server/server/drivers/chat-tool-approval.js +70 -18
- package/dist-server/server/drivers/claude.js +28 -8
- package/dist-server/server/drivers/codex-instructions.js +48 -6
- package/dist-server/server/drivers/codex-mcp-names.js +44 -0
- package/dist-server/server/drivers/codex-tool-surface.js +20 -0
- package/dist-server/server/drivers/codex.js +121 -34
- package/dist-server/server/drivers/grok.js +2 -1
- package/dist-server/server/drivers/mistral.js +115 -0
- package/dist-server/server/drivers/openai-chat.js +155 -32
- package/dist-server/server/drivers/openai-compat.js +16 -8
- package/dist-server/server/drivers/permission-command.js +35 -0
- package/dist-server/server/drivers/phone-proxy.js +47 -2
- package/dist-server/server/drivers/pi.js +75 -4
- package/dist-server/server/drivers/prompt-split.js +87 -0
- package/dist-server/server/enterprise.js +97 -16
- package/dist-server/server/external-runtime.js +83 -0
- package/dist-server/server/fleet.js +1 -1
- package/dist-server/server/group-thread-usage.js +103 -0
- package/dist-server/server/guarded-requests.js +60 -0
- package/dist-server/server/harness/http.js +61 -0
- package/dist-server/server/harness/registry.js +1 -0
- package/dist-server/server/hosted-models.js +151 -0
- package/dist-server/server/hosted-slack.js +11 -0
- package/dist-server/server/index.js +3091 -658
- package/dist-server/server/local-computer.js +72 -23
- package/dist-server/server/managed-desktop.js +79 -14
- package/dist-server/server/managed-policy.js +153 -0
- package/dist-server/server/mcp-bridge.js +6 -6
- package/dist-server/server/message-db.js +28 -0
- package/dist-server/server/model-context-window.js +7 -0
- package/dist-server/server/model-prices.js +121 -0
- package/dist-server/server/new-bot-defaults.js +115 -0
- package/dist-server/server/notify.js +10 -1
- package/dist-server/server/org-library.js +1051 -0
- package/dist-server/server/package-export.js +627 -39
- package/dist-server/server/package-import.js +458 -0
- package/dist-server/server/package-parts.js +87 -0
- package/dist-server/server/peer-approval.js +16 -7
- package/dist-server/server/peer-roster.js +23 -2
- package/dist-server/server/presets.js +492 -0
- package/dist-server/server/provider-key-check.js +3 -2
- package/dist-server/server/published-teams.js +80 -0
- package/dist-server/server/recent-work.js +15 -0
- package/dist-server/server/request-auth.js +96 -0
- package/dist-server/server/room-handoffs.js +44 -14
- package/dist-server/server/routes/bot-presets.js +25 -0
- package/dist-server/server/routes/hosted-slack.js +20 -0
- package/dist-server/server/routes/table.js +14 -0
- package/dist-server/server/routine-requests.js +6 -3
- package/dist-server/server/routines.js +63 -1
- package/dist-server/server/section-context.js +10 -0
- package/dist-server/server/skills.js +34 -61
- package/dist-server/server/spend.js +111 -16
- package/dist-server/server/steer-queue.js +9 -3
- package/dist-server/server/store.js +550 -35
- package/dist-server/server/surface.js +10 -4
- package/dist-server/server/system-prompt.js +12 -4
- package/dist-server/server/team-backup.js +16 -3
- package/dist-server/server/team-computers.js +10 -0
- package/dist-server/server/team-library.js +6 -2
- package/dist-server/server/team-manifest.js +4 -3
- package/dist-server/server/team-setup-requests.js +16 -5
- package/dist-server/server/thread-events.js +17 -1
- package/dist-server/server/thread-starters.js +53 -0
- package/dist-server/server/turn-context.js +2 -0
- package/dist-server/server/turn-watchdog.js +12 -3
- package/dist-server/server/usage-ledger.js +43 -9
- package/dist-server/server/vps-computer.js +139 -51
- package/dist-server/server/vps-container-mcp.js +7 -0
- package/dist-server/server/vps-ssh.js +36 -7
- package/dist-server/server/workspace-backup-policy.js +21 -2
- package/dist-server/server/workspace-backup.js +7 -4
- package/dist-server/server/workspace.js +1 -1
- package/dist-server/shared/approval-mode.js +1 -1
- package/dist-server/shared/ask-question.js +188 -12
- package/dist-server/shared/bot-profile.js +15 -0
- package/dist-server/shared/command-allowlist.js +1 -0
- package/dist-server/shared/credential-request.js +1 -1
- package/dist-server/shared/mention-boundary.js +15 -0
- package/dist-server/shared/package-format.js +967 -0
- package/dist-server/shared/routines.js +7 -1
- package/dist-server/shared/skill-md.js +64 -0
- package/dist-server/shared/team-backup.js +9 -0
- package/dist-server/shared/wire.js +4 -0
- package/dist-server/src/lib/composer-attachments.js +16 -0
- package/dist-server/vps-container-mcp.js +1147 -179
- package/enterprise/README.md +33 -12
- package/enterprise/server/index.js +5 -5
- package/package.json +1 -1
- package/dist/assets/index-Ba9G44HI.js +0 -310
- package/dist/assets/index-CkRNp7wX.css +0 -1
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
// ProviderSessionDirectory, recipe step 6: persist the binding from day
|
|
4
4
|
// one). messages-<threadId>.json holds the folded transcript.
|
|
5
5
|
import { createHash } from "node:crypto";
|
|
6
|
-
import { existsSync, readFileSync, mkdirSync, rmSync, unlinkSync } from "node:fs";
|
|
6
|
+
import { chmodSync, existsSync, readFileSync, mkdirSync, rmSync, statSync, unlinkSync } from "node:fs";
|
|
7
7
|
import { join } from "node:path";
|
|
8
8
|
import { writeFileAtomic } from "./atomic.js";
|
|
9
9
|
import { ensureSections, readSections, changeEmptySection } from "./section-context.js";
|
|
@@ -18,6 +18,8 @@ import { pickBotName } from "./names.js";
|
|
|
18
18
|
import { redactSecretsInText } from "./redact.js";
|
|
19
19
|
import { botAvatarProfile } from "../shared/bot-avatar.js";
|
|
20
20
|
import { approvalModeFor, isApprovalMode } from "../shared/approval-mode.js";
|
|
21
|
+
import { isMentionBoundary, isMentionNameContinuation } from "../shared/mention-boundary.js";
|
|
22
|
+
import { CONNECTOR_SLUG_PATTERN, CONNECTOR_TOOL_NAME_PATTERN } from "../shared/wire.js";
|
|
21
23
|
export const groupWireProjectionIsExact = true;
|
|
22
24
|
// Unicode's complete emoji sequences include flags, skin tones and ZWJ
|
|
23
25
|
// combinations. Also allow unqualified single symbols (e.g. ♥), but not
|
|
@@ -30,14 +32,23 @@ export const taskWireProjectionIsExact = true;
|
|
|
30
32
|
/** The typed wire projection for one task. Pairs with the assertion above:
|
|
31
33
|
* returning WireTask means an undeclared server field cannot ride silently. */
|
|
32
34
|
export function toWireTask(task) {
|
|
33
|
-
const { resumeCursors: _resumeCursors, lastInstanceId: _lastInstanceId, handedMessages: _handedMessages, appliedCompactionId: _appliedCompactionId, contextFloor: _contextFloor, lastContextModel: _lastContextModel, ...wire } = task;
|
|
35
|
+
const { resumeCursors: _resumeCursors, lastInstanceId: _lastInstanceId, handedMessages: _handedMessages, appliedCompactionId: _appliedCompactionId, contextFloor: _contextFloor, lastContextModel: _lastContextModel, surfaceSource: _surfaceSource, ...wire } = task;
|
|
34
36
|
return wire;
|
|
35
37
|
}
|
|
36
38
|
const TASK_PATCH_FIELDS = [
|
|
37
39
|
"title", "projectId", "modelSelection", "approvalMode", "autoApprove", "alwaysAllow",
|
|
38
|
-
"unread", "rewound", "archivedAt", "pinnedMessageId", "resumeCursors", "lastInstanceId", "cwd",
|
|
39
|
-
"routineRunId", "surface", "appliedCompactionId", "contextFloor", "lastContextModel",
|
|
40
|
+
"unread", "rewound", "archivedAt", "pinned", "pinnedMessageId", "resumeCursors", "lastInstanceId", "cwd",
|
|
41
|
+
"routineRunId", "surface", "surfaceSource", "snoozedUntil", "appliedCompactionId", "contextFloor", "lastContextModel",
|
|
40
42
|
];
|
|
43
|
+
/** Only `true` is a pin. false/undefined must not land in bots.json or groups.json. */
|
|
44
|
+
function persistedPin(task) {
|
|
45
|
+
if (task.pinned === true)
|
|
46
|
+
return task;
|
|
47
|
+
if (!("pinned" in task))
|
|
48
|
+
return task;
|
|
49
|
+
const { pinned: _pinned, ...rest } = task;
|
|
50
|
+
return rest;
|
|
51
|
+
}
|
|
41
52
|
/** Everything the BOT authored is scrubbed of content-shaped secrets before
|
|
42
53
|
* it is stored: its reply text, a tool title (an ACP engine's title can be
|
|
43
54
|
* the whole command line) and the command beside it, a permission card's
|
|
@@ -86,6 +97,9 @@ function redactBotAuthored(message) {
|
|
|
86
97
|
card.held = redactSecretsInText(card.held);
|
|
87
98
|
if (typeof card.answeredText === "string")
|
|
88
99
|
card.answeredText = redactSecretsInText(card.answeredText);
|
|
100
|
+
if (card.commandAllowlist && (redactSecretsInText(card.commandAllowlist.command) !== card.commandAllowlist.command ||
|
|
101
|
+
redactSecretsInText(card.commandAllowlist.cwd) !== card.commandAllowlist.cwd))
|
|
102
|
+
delete card.commandAllowlist;
|
|
89
103
|
// Bot-authored question text sits behind the subtitle the same way a
|
|
90
104
|
// routine's instructions do, so it is scrubbed on the same boundary.
|
|
91
105
|
if (card.questionRequest) {
|
|
@@ -252,8 +266,76 @@ export function titleFromLlm(raw) {
|
|
|
252
266
|
return line.length >= 1 && line.length <= 48 ? line : null;
|
|
253
267
|
}
|
|
254
268
|
export const botWireProjectionIsExact = true;
|
|
269
|
+
/** Upper bounds keep a grants patch from becoming a persistence blob; they
|
|
270
|
+
* sit far above any real service's tool count. */
|
|
271
|
+
export const CONNECTOR_SLUGS_MAX = 64;
|
|
272
|
+
export const CONNECTOR_TOOLS_PER_SERVICE_MAX = 500;
|
|
273
|
+
/** Validate and normalize a connectorTools value at the one boundary every
|
|
274
|
+
* writer shares (patchBot). Returns a canonical copy: slugs checked against
|
|
275
|
+
* the service pattern, tool lists deduplicated in order, `"*"` kept as-is.
|
|
276
|
+
* The legacy clear stays a JSON null at the API edge; here callers pass
|
|
277
|
+
* undefined to return a bot to boolean-only behavior. */
|
|
278
|
+
export function parseConnectorTools(value) {
|
|
279
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
280
|
+
return { ok: false, error: "connectorTools must be an object of service slugs to tool grants" };
|
|
281
|
+
}
|
|
282
|
+
const entries = Object.entries(value);
|
|
283
|
+
if (entries.length > CONNECTOR_SLUGS_MAX) {
|
|
284
|
+
return { ok: false, error: `connectorTools may name at most ${CONNECTOR_SLUGS_MAX} services` };
|
|
285
|
+
}
|
|
286
|
+
const grants = {};
|
|
287
|
+
for (const [slug, grant] of entries) {
|
|
288
|
+
if (!CONNECTOR_SLUG_PATTERN.test(slug)) {
|
|
289
|
+
return { ok: false, error: `connectorTools service slugs must be lowercase slugs (got "${slug}")` };
|
|
290
|
+
}
|
|
291
|
+
if (!grant || typeof grant !== "object" || Array.isArray(grant)) {
|
|
292
|
+
return { ok: false, error: `connectorTools.${slug} must be a grant like { tools: "*" } or { tools: ["TOOL_NAME"] }` };
|
|
293
|
+
}
|
|
294
|
+
const keys = Object.keys(grant);
|
|
295
|
+
if (keys.length !== 1 || keys[0] !== "tools") {
|
|
296
|
+
return { ok: false, error: `connectorTools.${slug} accepts only a tools field` };
|
|
297
|
+
}
|
|
298
|
+
const tools = grant.tools;
|
|
299
|
+
if (tools === "*") {
|
|
300
|
+
grants[slug] = { tools: "*" };
|
|
301
|
+
continue;
|
|
302
|
+
}
|
|
303
|
+
if (!Array.isArray(tools) || tools.length === 0) {
|
|
304
|
+
return { ok: false, error: `connectorTools.${slug}.tools must be "*" or a non-empty list of tool names (use {} to grant no tools)` };
|
|
305
|
+
}
|
|
306
|
+
if (tools.length > CONNECTOR_TOOLS_PER_SERVICE_MAX) {
|
|
307
|
+
return { ok: false, error: `connectorTools.${slug}.tools may list at most ${CONNECTOR_TOOLS_PER_SERVICE_MAX} tools` };
|
|
308
|
+
}
|
|
309
|
+
const names = [];
|
|
310
|
+
for (const tool of tools) {
|
|
311
|
+
if (typeof tool !== "string" || !CONNECTOR_TOOL_NAME_PATTERN.test(tool)) {
|
|
312
|
+
return { ok: false, error: `connectorTools.${slug}.tools names must be Composio tool names like GMAIL_SEND_EMAIL` };
|
|
313
|
+
}
|
|
314
|
+
if (!names.includes(tool))
|
|
315
|
+
names.push(tool);
|
|
316
|
+
}
|
|
317
|
+
grants[slug] = { tools: names };
|
|
318
|
+
}
|
|
319
|
+
return { ok: true, grants };
|
|
320
|
+
}
|
|
255
321
|
const BOTS_FILE = join(DATA_DIR, "bots.json");
|
|
256
322
|
const GROUPS_FILE = join(DATA_DIR, "groups.json");
|
|
323
|
+
/** The registries carry souls, project paths and per-bot settings, so they are
|
|
324
|
+
* owner-only like the other data-dir stores. A file written by an older
|
|
325
|
+
* release (or loosened by hand) is tightened on load. Best effort: failing to
|
|
326
|
+
* tighten must never stop the fleet from loading, and the next save replaces
|
|
327
|
+
* the file with a 0600 one anyway. Windows has no POSIX mode bits. */
|
|
328
|
+
function tightenRegistryFile(file) {
|
|
329
|
+
if (process.platform === "win32")
|
|
330
|
+
return;
|
|
331
|
+
try {
|
|
332
|
+
if ((statSync(file).mode & 0o077) !== 0)
|
|
333
|
+
chmodSync(file, 0o600);
|
|
334
|
+
}
|
|
335
|
+
catch {
|
|
336
|
+
/* absent, or not ours to change */
|
|
337
|
+
}
|
|
338
|
+
}
|
|
257
339
|
const messagesFile = (threadId) => join(DATA_DIR, `messages-${threadId}.json`);
|
|
258
340
|
const COLORS = [
|
|
259
341
|
"green",
|
|
@@ -283,15 +365,15 @@ export function mentionedBots(text, peers) {
|
|
|
283
365
|
const found = [];
|
|
284
366
|
let at = -1;
|
|
285
367
|
while ((at = lower.indexOf("@", at + 1)) !== -1) {
|
|
286
|
-
if (
|
|
368
|
+
if (!isMentionBoundary(text, at))
|
|
287
369
|
continue; // user@host, not a tag
|
|
288
370
|
const rest = lower.slice(at + 1);
|
|
289
371
|
const hit = candidates.find((p) => {
|
|
290
372
|
const name = p.name.toLowerCase();
|
|
291
373
|
if (!rest.startsWith(name))
|
|
292
374
|
return false;
|
|
293
|
-
const after = rest
|
|
294
|
-
return
|
|
375
|
+
const after = rest.slice(name.length); // must not run into a longer word
|
|
376
|
+
return !isMentionNameContinuation(after);
|
|
295
377
|
});
|
|
296
378
|
if (hit && !found.includes(hit))
|
|
297
379
|
found.push(hit);
|
|
@@ -324,8 +406,15 @@ export function normalizeGroupDefaultResponder(value, memberIds, dm = false) {
|
|
|
324
406
|
* otherwise the room policy chooses one member, everyone, or nobody. */
|
|
325
407
|
export function roomResponders(text, members, defaultResponder) {
|
|
326
408
|
const available = members.filter((member) => !member.hidden);
|
|
327
|
-
|
|
328
|
-
|
|
409
|
+
const everyone = "everyone";
|
|
410
|
+
for (let at = text.indexOf("@"); at !== -1; at = text.indexOf("@", at + 1)) {
|
|
411
|
+
const candidate = text.slice(at + 1, at + 1 + everyone.length);
|
|
412
|
+
if (isMentionBoundary(text, at) &&
|
|
413
|
+
candidate.toLocaleLowerCase() === everyone &&
|
|
414
|
+
!isMentionNameContinuation(text.slice(at + 1 + everyone.length))) {
|
|
415
|
+
return available;
|
|
416
|
+
}
|
|
417
|
+
}
|
|
329
418
|
const mentioned = mentionedBots(text, available);
|
|
330
419
|
if (mentioned.length)
|
|
331
420
|
return mentioned;
|
|
@@ -342,15 +431,22 @@ export class Store {
|
|
|
342
431
|
groups = [];
|
|
343
432
|
threads = new Map();
|
|
344
433
|
defaultSelection;
|
|
434
|
+
completeNewBotSelection;
|
|
345
435
|
listeners = new Set();
|
|
346
436
|
/** A broken team registry must not prevent loading independent chat data. */
|
|
347
437
|
registeringInitialSections = true;
|
|
348
438
|
/** Room turns and old callers have their own activity slot. Clearing
|
|
349
439
|
* that slot must not clear a concurrently running independent task. */
|
|
350
440
|
legacyActivities = new Map();
|
|
351
|
-
constructor(defaultSelection
|
|
441
|
+
constructor(defaultSelection,
|
|
442
|
+
/** Workspace-wide new-bot defaults (config newBots), applied to every
|
|
443
|
+
* new bot's selection whichever path created it. */
|
|
444
|
+
completeNewBotSelection = (selection) => selection) {
|
|
352
445
|
this.defaultSelection = defaultSelection;
|
|
446
|
+
this.completeNewBotSelection = completeNewBotSelection;
|
|
353
447
|
mkdirSync(DATA_DIR, { recursive: true });
|
|
448
|
+
for (const file of [BOTS_FILE, GROUPS_FILE])
|
|
449
|
+
tightenRegistryFile(file);
|
|
354
450
|
try {
|
|
355
451
|
this.bots = JSON.parse(readFileSync(BOTS_FILE, "utf8"));
|
|
356
452
|
}
|
|
@@ -517,6 +613,7 @@ export class Store {
|
|
|
517
613
|
threadId: g.threadId,
|
|
518
614
|
title: this.firstUserLine(g.threadId) ?? UNTITLED_TASK,
|
|
519
615
|
createdAt: g.createdAt,
|
|
616
|
+
updatedAt: g.createdAt,
|
|
520
617
|
};
|
|
521
618
|
if (g.pinnedCwd !== undefined)
|
|
522
619
|
initialTask.pinnedCwd = g.pinnedCwd;
|
|
@@ -552,6 +649,7 @@ export class Store {
|
|
|
552
649
|
threadId: b.threadId,
|
|
553
650
|
title: this.firstUserLine(b.threadId) ?? UNTITLED_TASK,
|
|
554
651
|
createdAt: b.createdAt,
|
|
652
|
+
updatedAt: b.createdAt,
|
|
555
653
|
resumeCursors: b.resumeCursors ?? {},
|
|
556
654
|
}];
|
|
557
655
|
botsMigrated = true;
|
|
@@ -564,6 +662,7 @@ export class Store {
|
|
|
564
662
|
threadId: b.threadId,
|
|
565
663
|
title: this.firstUserLine(b.threadId) ?? UNTITLED_TASK,
|
|
566
664
|
createdAt: b.createdAt,
|
|
665
|
+
updatedAt: b.createdAt,
|
|
567
666
|
resumeCursors: b.resumeCursors ?? {},
|
|
568
667
|
};
|
|
569
668
|
b.tasks.unshift(active);
|
|
@@ -619,18 +718,126 @@ export class Store {
|
|
|
619
718
|
if (existsSync(legacyFile))
|
|
620
719
|
mdb.readThread(threadId, legacyFile);
|
|
621
720
|
}
|
|
721
|
+
// After legacy transcripts are in SQLite, so the first boot sees their
|
|
722
|
+
// newest message instead of stamping createdAt and jumping next launch.
|
|
723
|
+
this.repairThreadUpdatedAts();
|
|
724
|
+
// After the roster is loaded, so identity resolution sees which opener
|
|
725
|
+
// ids are still live.
|
|
726
|
+
this.repairDuplicatePairConversations();
|
|
622
727
|
this.registeringInitialSections = false;
|
|
623
728
|
}
|
|
624
|
-
|
|
625
|
-
|
|
729
|
+
/** Advance a task's update stamp in memory only. Message writes must not
|
|
730
|
+
* rewrite bots.json; the next snapshot and the startup repair read this. */
|
|
731
|
+
noteThreadActivity(threadId, at) {
|
|
732
|
+
if (!Number.isFinite(at))
|
|
733
|
+
return;
|
|
734
|
+
const advance = (current) => Math.max(current ?? 0, at);
|
|
735
|
+
for (const bot of this.bots) {
|
|
736
|
+
const task = bot.tasks?.find((candidate) => candidate.threadId === threadId);
|
|
737
|
+
if (!task)
|
|
738
|
+
continue;
|
|
739
|
+
const next = advance(task.updatedAt);
|
|
740
|
+
if (task.updatedAt !== next)
|
|
741
|
+
task.updatedAt = next;
|
|
742
|
+
}
|
|
743
|
+
for (const group of this.groups) {
|
|
744
|
+
const task = group.tasks?.find((candidate) => candidate.threadId === threadId);
|
|
745
|
+
if (!task)
|
|
746
|
+
continue;
|
|
747
|
+
const next = advance(task.updatedAt);
|
|
748
|
+
if (task.updatedAt !== next)
|
|
749
|
+
task.updatedAt = next;
|
|
750
|
+
}
|
|
751
|
+
}
|
|
752
|
+
/** Fill missing stamps, and raise a stamp that is older than the newest
|
|
753
|
+
* stored message. Never moves a stamp backwards: there is no per-message
|
|
754
|
+
* delete, and a clock skew must not reshuffle the list on every boot. */
|
|
755
|
+
repairThreadUpdatedAts() {
|
|
756
|
+
const botTasks = this.bots.flatMap((bot) => bot.tasks ?? []);
|
|
757
|
+
const groupTasks = this.groups.flatMap((group) => group.tasks ?? []);
|
|
758
|
+
const latest = mdb.latestMessageAts([...botTasks, ...groupTasks].map((task) => task.threadId));
|
|
759
|
+
let botsDirty = false;
|
|
760
|
+
let groupsDirty = false;
|
|
761
|
+
const repair = (task) => {
|
|
762
|
+
const fromMessages = latest.get(task.threadId);
|
|
763
|
+
const next = fromMessages !== undefined && (task.updatedAt === undefined || fromMessages > task.updatedAt)
|
|
764
|
+
? fromMessages
|
|
765
|
+
: task.updatedAt ?? task.createdAt;
|
|
766
|
+
if (task.updatedAt === next)
|
|
767
|
+
return false;
|
|
768
|
+
task.updatedAt = next;
|
|
769
|
+
return true;
|
|
770
|
+
};
|
|
771
|
+
for (const task of botTasks)
|
|
772
|
+
if (repair(task))
|
|
773
|
+
botsDirty = true;
|
|
774
|
+
for (const task of groupTasks)
|
|
775
|
+
if (repair(task))
|
|
776
|
+
groupsDirty = true;
|
|
777
|
+
if (botsDirty)
|
|
778
|
+
this.saveBots();
|
|
779
|
+
if (groupsDirty)
|
|
780
|
+
this.saveGroups();
|
|
781
|
+
}
|
|
782
|
+
/** At most one live pair row per (recipient, sender identity). Servers
|
|
783
|
+
* before identity-stable matching minted a second live pair row for a
|
|
784
|
+
* deleted-and-recreated sender while the predecessor's row dangled live
|
|
785
|
+
* forever; collapse those on load by keeping the row the resolver favors
|
|
786
|
+
* (first in the task list — the newest, actively used one) and demoting
|
|
787
|
+
* the rest to closed plain threads. History is kept, never deleted, and
|
|
788
|
+
* a demoted row is never re-adopted: adoption skips closed rows. Runs on
|
|
789
|
+
* every load and touches nothing in a store that already holds the
|
|
790
|
+
* invariant. */
|
|
791
|
+
repairDuplicatePairConversations() {
|
|
792
|
+
let botsDirty = false;
|
|
793
|
+
const sweep = (tasks, repaired) => {
|
|
794
|
+
const pairs = new Map();
|
|
795
|
+
for (const task of tasks ?? []) {
|
|
796
|
+
const by = task.openedBy;
|
|
797
|
+
if (by?.kind !== "pair" || task.closedBy)
|
|
798
|
+
continue;
|
|
799
|
+
const identity = this.openerIdentity(by);
|
|
800
|
+
pairs.set(identity, [...(pairs.get(identity) ?? []), task]);
|
|
801
|
+
}
|
|
802
|
+
for (const rows of pairs.values()) {
|
|
803
|
+
// rows are in task-list order, the same order the resolver's
|
|
804
|
+
// find() favors: the first is the one it keeps using.
|
|
805
|
+
for (const duplicate of rows.slice(1)) {
|
|
806
|
+
const by = duplicate.openedBy;
|
|
807
|
+
const identity = this.openerIdentity(by);
|
|
808
|
+
const { kind: _kind, ...opened } = by;
|
|
809
|
+
duplicate.openedBy = opened;
|
|
810
|
+
duplicate.closedBy = {
|
|
811
|
+
botId: identity,
|
|
812
|
+
name: this.bot(identity)?.name ?? by.name,
|
|
813
|
+
at: Date.now(),
|
|
814
|
+
};
|
|
815
|
+
repaired();
|
|
816
|
+
}
|
|
817
|
+
}
|
|
818
|
+
};
|
|
819
|
+
// Pair rows only ever live on bots: the resolver requires a bot
|
|
820
|
+
// recipient, and group tasks carry no peer stamps.
|
|
821
|
+
for (const bot of this.bots)
|
|
822
|
+
sweep(bot.tasks, () => { botsDirty = true; });
|
|
823
|
+
if (botsDirty)
|
|
824
|
+
this.saveBots();
|
|
825
|
+
}
|
|
826
|
+
saveBots(bots = this.bots, registerSections = true) {
|
|
827
|
+
if (registerSections)
|
|
828
|
+
this.rememberSections([...this.bots, ...bots].map((bot) => bot.section));
|
|
626
829
|
writeFileAtomic(BOTS_FILE, JSON.stringify(bots.map(({ busy: _busy, activity: _activity, ...bot }) => ({
|
|
627
830
|
...bot,
|
|
628
|
-
tasks: bot.tasks?.map(({ busy: _taskBusy, activity: _taskActivity, turnStartedAt: _taskTurnStarted, ...task }) => task),
|
|
629
|
-
})), null, 2));
|
|
831
|
+
tasks: bot.tasks?.map(({ busy: _taskBusy, activity: _taskActivity, turnStartedAt: _taskTurnStarted, ...task }) => persistedPin(task)),
|
|
832
|
+
})), null, 2), { mode: 0o600 });
|
|
630
833
|
}
|
|
631
|
-
saveGroups() {
|
|
632
|
-
|
|
633
|
-
|
|
834
|
+
saveGroups(groups = this.groups, registerSections = true) {
|
|
835
|
+
if (registerSections)
|
|
836
|
+
this.rememberSections(groups.map((group) => group.section));
|
|
837
|
+
writeFileAtomic(GROUPS_FILE, JSON.stringify(groups.map(({ busyBotId: _busyBotId, turnStartedAt: _turnStartedAt, ...g }) => ({
|
|
838
|
+
...g,
|
|
839
|
+
...(g.tasks ? { tasks: g.tasks.map((task) => persistedPin(task)) } : {}),
|
|
840
|
+
})), null, 2), { mode: 0o600 });
|
|
634
841
|
}
|
|
635
842
|
get sections() { return readSections(); }
|
|
636
843
|
rememberSections(names) {
|
|
@@ -644,6 +851,60 @@ export class Store {
|
|
|
644
851
|
console.warn(`[teams] Startup could not register team names; saved teams and shared instructions were left unchanged: ${error.message}`);
|
|
645
852
|
}
|
|
646
853
|
}
|
|
854
|
+
/** Rename the same team, preserving its members and existing access grants. */
|
|
855
|
+
renameSection(name, nextName, computers) {
|
|
856
|
+
if (!name || !this.sections.includes(name))
|
|
857
|
+
return "No such team";
|
|
858
|
+
nextName = nextName.trim();
|
|
859
|
+
if (!nextName || nextName.length > 60 || [...nextName].some(char => char.charCodeAt(0) < 32 || char.charCodeAt(0) === 127))
|
|
860
|
+
return "Team name must be 1 to 60 characters without control characters";
|
|
861
|
+
if (name === nextName)
|
|
862
|
+
return undefined;
|
|
863
|
+
if (this.sections.includes(nextName) || computers?.forSection(nextName))
|
|
864
|
+
return "A team with that name already exists";
|
|
865
|
+
const members = this.bots.filter(bot => sectionKey(bot.section) === name);
|
|
866
|
+
const rooms = this.groups.filter(group => sectionKey(group.section) === name);
|
|
867
|
+
const affected = this.bots.filter(bot => members.includes(bot) || bot.managedSections?.some(section => sectionKey(section) === name));
|
|
868
|
+
if (affected.some(bot => bot.busy || bot.tasks?.some(task => task.busy)) || rooms.some(group => group.busyBotId)) {
|
|
869
|
+
return "Stop this team's active work before renaming the team";
|
|
870
|
+
}
|
|
871
|
+
// Keep the established empty-team lifecycle: old grants are revoked when
|
|
872
|
+
// an empty identity is removed, so recreating it cannot restore access.
|
|
873
|
+
if (!members.length && !rooms.length && !computers?.forSection(name))
|
|
874
|
+
return this.changeEmptySection(name, nextName);
|
|
875
|
+
const nextBots = this.bots.map(bot => ({ ...bot,
|
|
876
|
+
...(members.includes(bot) ? { section: nextName } : {}),
|
|
877
|
+
...(bot.managedSections ? { managedSections: [...new Set(bot.managedSections.map(section => sectionKey(section) === name ? nextName : section))] } : {}),
|
|
878
|
+
}));
|
|
879
|
+
const nextGroups = this.groups.map(group => rooms.includes(group) ? { ...group, section: nextName } : group);
|
|
880
|
+
let computerChanged = false;
|
|
881
|
+
try {
|
|
882
|
+
// Register the new name only after saving the records; otherwise the
|
|
883
|
+
// registry's duplicate-name check would reject this same rename.
|
|
884
|
+
this.saveBots(nextBots, false);
|
|
885
|
+
this.saveGroups(nextGroups, false);
|
|
886
|
+
computerChanged = computers?.renameSection(name, nextName) ?? false;
|
|
887
|
+
changeEmptySection(name, nextName);
|
|
888
|
+
}
|
|
889
|
+
catch (error) {
|
|
890
|
+
this.saveBots(this.bots, false);
|
|
891
|
+
this.saveGroups(this.groups, false);
|
|
892
|
+
if (computerChanged)
|
|
893
|
+
computers.renameSection(nextName, name);
|
|
894
|
+
throw error;
|
|
895
|
+
}
|
|
896
|
+
// Preserve identities held by the schedulers and other store consumers.
|
|
897
|
+
for (let i = 0; i < nextBots.length; i++)
|
|
898
|
+
Object.assign(this.bots[i], nextBots[i]);
|
|
899
|
+
for (let i = 0; i < nextGroups.length; i++)
|
|
900
|
+
Object.assign(this.groups[i], nextGroups[i]);
|
|
901
|
+
for (const bot of affected)
|
|
902
|
+
this.emit({ type: "bot", botId: bot.id });
|
|
903
|
+
for (const room of rooms)
|
|
904
|
+
this.emit({ type: "group", groupId: room.id });
|
|
905
|
+
this.emit({ type: "sections" });
|
|
906
|
+
return undefined;
|
|
907
|
+
}
|
|
647
908
|
/** Empty-only changes cannot merge teams or silently change anybody's access. */
|
|
648
909
|
changeEmptySection(name, nextName) {
|
|
649
910
|
if (!this.sections.includes(name))
|
|
@@ -672,6 +933,44 @@ export class Store {
|
|
|
672
933
|
this.emit({ type: "sections" });
|
|
673
934
|
return undefined;
|
|
674
935
|
}
|
|
936
|
+
/** Remove the team, keeping its bots, rooms and conversations in General. */
|
|
937
|
+
deleteSection(name) {
|
|
938
|
+
if (!name || !this.sections.includes(name))
|
|
939
|
+
return "No such team";
|
|
940
|
+
const members = this.bots.filter(bot => sectionKey(bot.section) === name);
|
|
941
|
+
const rooms = this.groups.filter(group => sectionKey(group.section) === name);
|
|
942
|
+
if (members.some(bot => bot.busy || bot.tasks?.some(task => task.busy)) || rooms.some(group => group.busyBotId)) {
|
|
943
|
+
return "Stop this team's active work before deleting the team";
|
|
944
|
+
}
|
|
945
|
+
if (this.bots.filter(bot => bot.chiefOfStaff && (!sectionKey(bot.section) || sectionKey(bot.section) === name)).length > 1) {
|
|
946
|
+
return "General already has a Chief of Staff. Move or change this team's Chief before deleting the team";
|
|
947
|
+
}
|
|
948
|
+
const nextBots = this.bots.map(bot => ({ ...bot,
|
|
949
|
+
...(sectionKey(bot.section) === name ? { section: undefined } : {}),
|
|
950
|
+
...(bot.managedSections ? { managedSections: bot.managedSections.filter(section => sectionKey(section) !== name) } : {}),
|
|
951
|
+
}));
|
|
952
|
+
const nextGroups = this.groups.map(group => sectionKey(group.section) === name ? { ...group, section: undefined } : group);
|
|
953
|
+
try {
|
|
954
|
+
this.saveBots(nextBots);
|
|
955
|
+
this.saveGroups(nextGroups);
|
|
956
|
+
changeEmptySection(name, null);
|
|
957
|
+
}
|
|
958
|
+
catch (error) {
|
|
959
|
+
this.saveBots();
|
|
960
|
+
this.saveGroups();
|
|
961
|
+
throw error;
|
|
962
|
+
}
|
|
963
|
+
for (let i = 0; i < nextBots.length; i++)
|
|
964
|
+
Object.assign(this.bots[i], nextBots[i]);
|
|
965
|
+
for (let i = 0; i < nextGroups.length; i++)
|
|
966
|
+
Object.assign(this.groups[i], nextGroups[i]);
|
|
967
|
+
for (const bot of this.bots)
|
|
968
|
+
this.emit({ type: "bot", botId: bot.id });
|
|
969
|
+
for (const group of rooms)
|
|
970
|
+
this.emit({ type: "group", groupId: group.id });
|
|
971
|
+
this.emit({ type: "sections" });
|
|
972
|
+
return undefined;
|
|
973
|
+
}
|
|
675
974
|
// ── groups ────────────────────────────────────────────────────────────
|
|
676
975
|
/** Subscribe to every write. Listeners run after the write and after
|
|
677
976
|
* save; a throwing listener never breaks the write. */
|
|
@@ -715,7 +1014,7 @@ export class Store {
|
|
|
715
1014
|
section,
|
|
716
1015
|
};
|
|
717
1016
|
if (!dm) {
|
|
718
|
-
group.tasks = [{ threadId, title: UNTITLED_TASK, createdAt }];
|
|
1017
|
+
group.tasks = [{ threadId, title: UNTITLED_TASK, createdAt, updatedAt: createdAt }];
|
|
719
1018
|
group.setupCompletedAt = setup?.completed ? createdAt : null;
|
|
720
1019
|
group.setupSkippedAt = null;
|
|
721
1020
|
}
|
|
@@ -847,10 +1146,12 @@ export class Store {
|
|
|
847
1146
|
const group = this.group(groupId);
|
|
848
1147
|
if (!group || group.dm)
|
|
849
1148
|
return null;
|
|
1149
|
+
const createdAt = Date.now();
|
|
850
1150
|
const task = {
|
|
851
1151
|
threadId: newId(),
|
|
852
1152
|
title: title?.trim().slice(0, 80) || UNTITLED_TASK,
|
|
853
|
-
createdAt
|
|
1153
|
+
createdAt,
|
|
1154
|
+
updatedAt: createdAt,
|
|
854
1155
|
};
|
|
855
1156
|
group.tasks = [task, ...(group.tasks ?? [])];
|
|
856
1157
|
if (activate) {
|
|
@@ -874,6 +1175,18 @@ export class Store {
|
|
|
874
1175
|
this.emit({ type: "group", groupId });
|
|
875
1176
|
return group;
|
|
876
1177
|
}
|
|
1178
|
+
setGroupTaskPinned(groupId, threadId, pinned) {
|
|
1179
|
+
const task = this.groupTaskByThread(groupId, threadId);
|
|
1180
|
+
if (!task)
|
|
1181
|
+
return null;
|
|
1182
|
+
if (pinned)
|
|
1183
|
+
task.pinned = true;
|
|
1184
|
+
else
|
|
1185
|
+
delete task.pinned;
|
|
1186
|
+
this.saveGroups();
|
|
1187
|
+
this.emit({ type: "group", groupId });
|
|
1188
|
+
return task;
|
|
1189
|
+
}
|
|
877
1190
|
renameGroupTask(groupId, threadId, title) {
|
|
878
1191
|
const task = this.groupTaskByThread(groupId, threadId);
|
|
879
1192
|
if (!task)
|
|
@@ -1003,6 +1316,9 @@ export class Store {
|
|
|
1003
1316
|
throw new Error("Cannot import over an existing conversation");
|
|
1004
1317
|
mdb.importThread(threadId, messages, activeLeafId);
|
|
1005
1318
|
this.threads.delete(threadId);
|
|
1319
|
+
const newest = messages.reduce((max, message) => Math.max(max, message.at), Number.NEGATIVE_INFINITY);
|
|
1320
|
+
if (Number.isFinite(newest))
|
|
1321
|
+
this.noteThreadActivity(threadId, newest);
|
|
1006
1322
|
}
|
|
1007
1323
|
activeLeaf(threadId) {
|
|
1008
1324
|
return this.thread(threadId).activeLeafId;
|
|
@@ -1051,6 +1367,7 @@ export class Store {
|
|
|
1051
1367
|
this.emit({ type: "message.patch", threadId, message: pruned });
|
|
1052
1368
|
}
|
|
1053
1369
|
}
|
|
1370
|
+
this.noteThreadActivity(threadId, full.at);
|
|
1054
1371
|
this.emit({ type: "message", threadId, message: full });
|
|
1055
1372
|
// The first-run quiz is not a live ask. Talking past it hides it so the
|
|
1056
1373
|
// transcript is just the greeting plus what they said. Cards with a
|
|
@@ -1081,6 +1398,7 @@ export class Store {
|
|
|
1081
1398
|
this.emit({ type: "message.patch", threadId, message: pruned });
|
|
1082
1399
|
}
|
|
1083
1400
|
}
|
|
1401
|
+
this.noteThreadActivity(threadId, full.at);
|
|
1084
1402
|
this.emit({ type: "message", threadId, message: full });
|
|
1085
1403
|
// announced after the insert so no client ever sees two siblings
|
|
1086
1404
|
// claiming the same parent
|
|
@@ -1118,8 +1436,10 @@ export class Store {
|
|
|
1118
1436
|
return pruned;
|
|
1119
1437
|
}
|
|
1120
1438
|
/** Fork the conversation: a new user message that replaces `sourceId`
|
|
1121
|
-
* (same parent, new text) and becomes the active leaf.
|
|
1122
|
-
|
|
1439
|
+
* (same parent, new text) and becomes the active leaf. `sendId` is the
|
|
1440
|
+
* client's identity for this edit, so its instant bubble reconciles onto
|
|
1441
|
+
* the canonical message and a network retry cannot fork twice. */
|
|
1442
|
+
branchMessage(threadId, sourceId, text, sendId) {
|
|
1123
1443
|
const t = this.thread(threadId);
|
|
1124
1444
|
const source = t.messages.find((m) => m.id === sourceId);
|
|
1125
1445
|
if (!source)
|
|
@@ -1132,10 +1452,12 @@ export class Store {
|
|
|
1132
1452
|
text,
|
|
1133
1453
|
parentId: source.parentId ?? null,
|
|
1134
1454
|
replyToId: source.replyToId,
|
|
1455
|
+
...(sendId ? { sendId } : {}),
|
|
1135
1456
|
};
|
|
1136
1457
|
t.messages.push(full);
|
|
1137
1458
|
t.activeLeafId = full.id;
|
|
1138
1459
|
mdb.appendMessage(threadId, full);
|
|
1460
|
+
this.noteThreadActivity(threadId, full.at);
|
|
1139
1461
|
this.emit({ type: "message", threadId, message: full });
|
|
1140
1462
|
// The message frame alone leaves every client on the OLD branch: a
|
|
1141
1463
|
// client adopts a new message as its leaf only when it chains onto the
|
|
@@ -1188,6 +1510,11 @@ export class Store {
|
|
|
1188
1510
|
botByThread(threadId) {
|
|
1189
1511
|
return this.bots.find((b) => b.threadId === threadId || b.tasks?.some((t) => t.threadId === threadId)) ?? null;
|
|
1190
1512
|
}
|
|
1513
|
+
/** The one place a new bot's selection is decided: the caller's choice, or
|
|
1514
|
+
* the workspace default, completed with the workspace's new-bot defaults. */
|
|
1515
|
+
newBotSelection(requested) {
|
|
1516
|
+
return this.completeNewBotSelection(requested ?? this.defaultSelection());
|
|
1517
|
+
}
|
|
1191
1518
|
createBot(profile = {}, opts = {}) {
|
|
1192
1519
|
this.rememberSections([profile.section]);
|
|
1193
1520
|
const name = profile.name?.trim() || pickBotName(this.bots.map((b) => b.name));
|
|
@@ -1204,8 +1531,10 @@ export class Store {
|
|
|
1204
1531
|
color: profile.color ?? COLORS[this.bots.length % COLORS.length],
|
|
1205
1532
|
...(profile.mascotExpression ? { mascotExpression: profile.mascotExpression } : {}),
|
|
1206
1533
|
...(profile.mascotBody ? { mascotBody: profile.mascotBody } : {}),
|
|
1534
|
+
// Restricted from its first frame: no one else is ever told it exists.
|
|
1535
|
+
...(profile.visibility && profile.visibility !== "everyone" ? { visibility: structuredClone(profile.visibility) } : {}),
|
|
1207
1536
|
unread: false,
|
|
1208
|
-
modelSelection: profile.modelSelection
|
|
1537
|
+
modelSelection: this.newBotSelection(profile.modelSelection),
|
|
1209
1538
|
resumeCursors: {},
|
|
1210
1539
|
createdAt: Date.now(),
|
|
1211
1540
|
};
|
|
@@ -1215,6 +1544,7 @@ export class Store {
|
|
|
1215
1544
|
threadId: bot.threadId,
|
|
1216
1545
|
title: UNTITLED_THREAD,
|
|
1217
1546
|
createdAt: bot.createdAt,
|
|
1547
|
+
updatedAt: bot.createdAt,
|
|
1218
1548
|
resumeCursors: {},
|
|
1219
1549
|
modelSelection: structuredClone(bot.modelSelection),
|
|
1220
1550
|
unread: false,
|
|
@@ -1266,12 +1596,16 @@ export class Store {
|
|
|
1266
1596
|
if (at >= 0 || !operation.threadId || !operation.fields.name || !operation.fields.modelSelection)
|
|
1267
1597
|
throw new Error("Invalid new bot in team setup");
|
|
1268
1598
|
const createdAt = Date.now();
|
|
1599
|
+
const modelSelection = this.newBotSelection(operation.fields.modelSelection);
|
|
1269
1600
|
next = { id: operation.botId, threadId: operation.threadId, name: operation.fields.name,
|
|
1270
1601
|
title: "", description: "", soul: "", notifications: true, color: COLORS[nextBots.length % COLORS.length], unread: false,
|
|
1271
|
-
|
|
1602
|
+
resumeCursors: {}, createdAt, ...operation.fields, modelSelection,
|
|
1272
1603
|
approvalMode: "ask", autoApprove: false, composio: false, approvePeerComms: false,
|
|
1273
|
-
|
|
1274
|
-
|
|
1604
|
+
// A Chief's new teammate is seen by exactly the Chief's audience:
|
|
1605
|
+
// a restricted Chief never creates a bot everyone sees.
|
|
1606
|
+
...(chief.visibility && chief.visibility !== "everyone" ? { visibility: structuredClone(chief.visibility) } : {}),
|
|
1607
|
+
tasks: [{ threadId: operation.threadId, title: UNTITLED_THREAD, createdAt, updatedAt: createdAt, resumeCursors: {},
|
|
1608
|
+
modelSelection: structuredClone(modelSelection), approvalMode: "ask", autoApprove: false,
|
|
1275
1609
|
unread: false, activity: "idle", busy: false }],
|
|
1276
1610
|
};
|
|
1277
1611
|
nextBots.unshift(next);
|
|
@@ -1291,6 +1625,8 @@ export class Store {
|
|
|
1291
1625
|
}));
|
|
1292
1626
|
nextBots[at] = next;
|
|
1293
1627
|
}
|
|
1628
|
+
if (operation.fields.chiefOfStaff === false)
|
|
1629
|
+
delete next.managedSections;
|
|
1294
1630
|
next.section = sectionKey(next.section) || undefined;
|
|
1295
1631
|
if (operation.fields.soul !== undefined) {
|
|
1296
1632
|
next.soulHash = soulHash(operation.fields.soul);
|
|
@@ -1299,14 +1635,14 @@ export class Store {
|
|
|
1299
1635
|
changed.push(next);
|
|
1300
1636
|
}
|
|
1301
1637
|
const result = { state: "applied", newTeams: request.newTeams, bots: changed.map((bot, index) => ({
|
|
1302
|
-
id: bot.id, name: bot.name, section: bot.section, modelSelection: structuredClone(bot.modelSelection),
|
|
1638
|
+
id: bot.id, name: bot.name, section: bot.section, modelSelection: structuredClone(bot.modelSelection), chiefOfStaff: Boolean(bot.chiefOfStaff),
|
|
1303
1639
|
action: request.operations[index].action === "create" ? "created" : "updated",
|
|
1304
1640
|
})) };
|
|
1305
1641
|
const chiefAt = nextBots.findIndex((bot) => bot.id === chief.id);
|
|
1306
1642
|
const nextChief = { ...nextBots[chiefAt], lastTeamSetupReceipt: { requestId: request.requestId, result } };
|
|
1307
1643
|
// Only the newly-created teams explicitly named in the human review may
|
|
1308
1644
|
// extend this Chief's reach. Existing teams require owner settings.
|
|
1309
|
-
if (request.newTeams.length) {
|
|
1645
|
+
if (request.newTeams.length && nextChief.chiefOfStaff) {
|
|
1310
1646
|
nextChief.managedSections = managedSections;
|
|
1311
1647
|
}
|
|
1312
1648
|
nextBots[chiefAt] = nextChief;
|
|
@@ -1371,6 +1707,16 @@ export class Store {
|
|
|
1371
1707
|
const bot = this.bot(id);
|
|
1372
1708
|
if (!bot)
|
|
1373
1709
|
return null;
|
|
1710
|
+
// Grants are the one field whose shape every writer must share, so the
|
|
1711
|
+
// store normalizes them itself: API patches arrive pre-validated, import
|
|
1712
|
+
// paths pass {}, and an internal caller that skips the parser still
|
|
1713
|
+
// lands canonical data or stops here.
|
|
1714
|
+
if (patch.connectorTools !== undefined) {
|
|
1715
|
+
const parsed = parseConnectorTools(patch.connectorTools);
|
|
1716
|
+
if (!parsed.ok)
|
|
1717
|
+
throw new Error(parsed.error);
|
|
1718
|
+
patch = { ...patch, connectorTools: parsed.grants };
|
|
1719
|
+
}
|
|
1374
1720
|
// Runtime revocations must become effective in memory even when disk is
|
|
1375
1721
|
// unavailable. Profile edits use the separate atomic path below.
|
|
1376
1722
|
Object.assign(bot, patch);
|
|
@@ -1481,6 +1827,39 @@ export class Store {
|
|
|
1481
1827
|
this.rememberSections([targetSection]);
|
|
1482
1828
|
return { ok: true, bots: ids.map((id) => this.bot(id)) };
|
|
1483
1829
|
}
|
|
1830
|
+
/** Apply only the membership edits the user made, in one bots-file write. */
|
|
1831
|
+
updateTeamMembers(section, addIds, removeIds) {
|
|
1832
|
+
const key = sectionKey(section);
|
|
1833
|
+
if (!key || !this.sections.includes(key))
|
|
1834
|
+
return { ok: false, reason: "unavailable" };
|
|
1835
|
+
const adds = new Set(addIds), removes = new Set(removeIds);
|
|
1836
|
+
const ids = new Set([...adds, ...removes]);
|
|
1837
|
+
for (const id of ids) {
|
|
1838
|
+
const bot = this.bot(id);
|
|
1839
|
+
if (!bot || bot.hidden)
|
|
1840
|
+
return { ok: false, reason: "unavailable" };
|
|
1841
|
+
if (adds.has(id) && removes.has(id))
|
|
1842
|
+
return { ok: false, reason: "membership-changed" };
|
|
1843
|
+
if (removes.has(id) && sectionKey(bot.section) !== key)
|
|
1844
|
+
return { ok: false, reason: "membership-changed" };
|
|
1845
|
+
}
|
|
1846
|
+
const next = this.bots.map(bot => ids.has(bot.id)
|
|
1847
|
+
? { ...bot, section: adds.has(bot.id) ? key : undefined } : bot);
|
|
1848
|
+
for (const destination of [key, ""]) {
|
|
1849
|
+
if (next.filter(bot => bot.chiefOfStaff && sectionKey(bot.section) === destination).length > 1) {
|
|
1850
|
+
return { ok: false, reason: "chief-conflict" };
|
|
1851
|
+
}
|
|
1852
|
+
}
|
|
1853
|
+
if (ids.size) {
|
|
1854
|
+
this.saveBots(next);
|
|
1855
|
+
for (let i = 0; i < next.length; i++)
|
|
1856
|
+
if (ids.has(next[i].id))
|
|
1857
|
+
Object.assign(this.bots[i], next[i]);
|
|
1858
|
+
for (const botId of ids)
|
|
1859
|
+
this.emit({ type: "bot", botId });
|
|
1860
|
+
}
|
|
1861
|
+
return { ok: true, bots: this.bots.filter(bot => ids.has(bot.id)) };
|
|
1862
|
+
}
|
|
1484
1863
|
/** Legacy bot/room activity occupies its own slot; direct conversations
|
|
1485
1864
|
* use setTaskActivity so settling one thread cannot clear another. */
|
|
1486
1865
|
setActivity(botId, activity) {
|
|
@@ -1509,6 +1888,13 @@ export class Store {
|
|
|
1509
1888
|
task.turnStartedAt = Date.now();
|
|
1510
1889
|
else if (!busy)
|
|
1511
1890
|
delete task.turnStartedAt;
|
|
1891
|
+
// Reaching here means the thread just did something — exactly the
|
|
1892
|
+
// "activity" an until-activity snooze waits for, including settling
|
|
1893
|
+
// back to idle after a turn. Persist the wake like any task change.
|
|
1894
|
+
if (task.snoozedUntil === 0) {
|
|
1895
|
+
task.snoozedUntil = undefined;
|
|
1896
|
+
this.saveBots();
|
|
1897
|
+
}
|
|
1512
1898
|
this.refreshBotActivity(bot);
|
|
1513
1899
|
this.emit({ type: "bot", botId });
|
|
1514
1900
|
return bot;
|
|
@@ -1551,6 +1937,64 @@ export class Store {
|
|
|
1551
1937
|
this.emit({ type: "bot", botId: bot.id });
|
|
1552
1938
|
return changed;
|
|
1553
1939
|
}
|
|
1940
|
+
/** Company instance ids became stable across re-enrolment. Moves every
|
|
1941
|
+
* saved reference to an old id onto its replacement in one save: model
|
|
1942
|
+
* choices, native resume cursors and handed-message records. An entry that
|
|
1943
|
+
* already exists under the new id wins over the old one. */
|
|
1944
|
+
renameInstances(ids) {
|
|
1945
|
+
const touches = (record) => Boolean(record && Object.keys(record).some(key => ids.has(key)));
|
|
1946
|
+
const rename = (record) => {
|
|
1947
|
+
const next = {};
|
|
1948
|
+
for (const [key, value] of Object.entries(record)) {
|
|
1949
|
+
const target = ids.get(key);
|
|
1950
|
+
if (!target)
|
|
1951
|
+
next[key] = value;
|
|
1952
|
+
else if (!Object.prototype.hasOwnProperty.call(record, target))
|
|
1953
|
+
next[target] = value;
|
|
1954
|
+
}
|
|
1955
|
+
return next;
|
|
1956
|
+
};
|
|
1957
|
+
const changed = [];
|
|
1958
|
+
for (const bot of this.bots) {
|
|
1959
|
+
let dirty = false;
|
|
1960
|
+
const selected = ids.get(bot.modelSelection.instanceId);
|
|
1961
|
+
if (selected) {
|
|
1962
|
+
bot.modelSelection = { ...bot.modelSelection, instanceId: selected };
|
|
1963
|
+
dirty = true;
|
|
1964
|
+
}
|
|
1965
|
+
if (touches(bot.resumeCursors)) {
|
|
1966
|
+
bot.resumeCursors = rename(bot.resumeCursors);
|
|
1967
|
+
dirty = true;
|
|
1968
|
+
}
|
|
1969
|
+
for (const task of bot.tasks ?? []) {
|
|
1970
|
+
const taskSelected = task.modelSelection && ids.get(task.modelSelection.instanceId);
|
|
1971
|
+
if (task.modelSelection && taskSelected) {
|
|
1972
|
+
task.modelSelection = { ...task.modelSelection, instanceId: taskSelected };
|
|
1973
|
+
dirty = true;
|
|
1974
|
+
}
|
|
1975
|
+
if (touches(task.resumeCursors)) {
|
|
1976
|
+
task.resumeCursors = rename(task.resumeCursors);
|
|
1977
|
+
dirty = true;
|
|
1978
|
+
}
|
|
1979
|
+
if (task.handedMessages && touches(task.handedMessages)) {
|
|
1980
|
+
task.handedMessages = rename(task.handedMessages);
|
|
1981
|
+
dirty = true;
|
|
1982
|
+
}
|
|
1983
|
+
const last = task.lastInstanceId && ids.get(task.lastInstanceId);
|
|
1984
|
+
if (last) {
|
|
1985
|
+
task.lastInstanceId = last;
|
|
1986
|
+
dirty = true;
|
|
1987
|
+
}
|
|
1988
|
+
}
|
|
1989
|
+
if (dirty)
|
|
1990
|
+
changed.push(bot);
|
|
1991
|
+
}
|
|
1992
|
+
if (changed.length)
|
|
1993
|
+
this.saveBots();
|
|
1994
|
+
for (const bot of changed)
|
|
1995
|
+
this.emit({ type: "bot", botId: bot.id });
|
|
1996
|
+
return changed.length;
|
|
1997
|
+
}
|
|
1554
1998
|
setResumeCursor(botId, instanceId, cursor, threadId) {
|
|
1555
1999
|
const bot = this.bot(botId);
|
|
1556
2000
|
if (!bot)
|
|
@@ -1795,8 +2239,16 @@ export class Store {
|
|
|
1795
2239
|
Object.assign(task, { [key]: structuredClone(patch[key]) });
|
|
1796
2240
|
}
|
|
1797
2241
|
}
|
|
2242
|
+
// "Until new activity" ends the moment the thread has something new for
|
|
2243
|
+
// the person, and every unread wake funnels through patchTask — so this
|
|
2244
|
+
// one hook is the whole activity alarm. A time-based snooze is left to
|
|
2245
|
+
// its clock: attention overrides it on screen without clearing it.
|
|
2246
|
+
if (task.snoozedUntil === 0 && patch.unread === true)
|
|
2247
|
+
task.snoozedUntil = undefined;
|
|
1798
2248
|
if (typeof patch.title === "string")
|
|
1799
2249
|
task.title = patch.title.trim().slice(0, 80) || UNTITLED_THREAD;
|
|
2250
|
+
if (Object.prototype.hasOwnProperty.call(patch, "pinned") && task.pinned !== true)
|
|
2251
|
+
delete task.pinned;
|
|
1800
2252
|
if (bot.threadId === threadId)
|
|
1801
2253
|
this.mirrorActiveTask(bot, task);
|
|
1802
2254
|
bot.unread = bot.tasks.some((candidate) => candidate.unread);
|
|
@@ -1804,6 +2256,28 @@ export class Store {
|
|
|
1804
2256
|
this.emit({ type: "bot", botId });
|
|
1805
2257
|
return task;
|
|
1806
2258
|
}
|
|
2259
|
+
/** A Works on change is the newest explicit choice, so this bot's
|
|
2260
|
+
* machine-recorded pins that now point somewhere else give way. A pin a
|
|
2261
|
+
* person set, a legacy pin with unknown provenance, and a pin that already
|
|
2262
|
+
* matches the new destination survive. Returns how many pins were cleared. */
|
|
2263
|
+
clearAutoSurfacePins(botId, destination) {
|
|
2264
|
+
const bot = this.bot(botId);
|
|
2265
|
+
if (!bot?.tasks)
|
|
2266
|
+
return 0;
|
|
2267
|
+
let cleared = 0;
|
|
2268
|
+
for (const task of bot.tasks) {
|
|
2269
|
+
if (task.surface === undefined || task.surfaceSource !== "auto" || task.surface === destination)
|
|
2270
|
+
continue;
|
|
2271
|
+
task.surface = undefined;
|
|
2272
|
+
task.surfaceSource = undefined;
|
|
2273
|
+
cleared++;
|
|
2274
|
+
}
|
|
2275
|
+
if (!cleared)
|
|
2276
|
+
return 0;
|
|
2277
|
+
this.saveBots();
|
|
2278
|
+
this.emit({ type: "bot", botId });
|
|
2279
|
+
return cleared;
|
|
2280
|
+
}
|
|
1807
2281
|
/** Model/provider changes are one configuration transaction: never publish
|
|
1808
2282
|
* a new provider before its confirmed approval downgrade, or change the
|
|
1809
2283
|
* default while leaving the selected thread behind after a write failure. */
|
|
@@ -1814,8 +2288,8 @@ export class Store {
|
|
|
1814
2288
|
return null;
|
|
1815
2289
|
const patch = { modelSelection: structuredClone(selection),
|
|
1816
2290
|
...(resetApprovalToAsk ? { approvalMode: "ask", autoApprove: false, alwaysAllow: [] } : {}) };
|
|
1817
|
-
const nextTask = { ...task, ...taskPatch, ...patch,
|
|
1818
|
-
...(typeof taskPatch.title === "string" ? { title: taskPatch.title.trim().slice(0, 80) || UNTITLED_THREAD } : {}) };
|
|
2291
|
+
const nextTask = persistedPin({ ...task, ...taskPatch, ...patch,
|
|
2292
|
+
...(typeof taskPatch.title === "string" ? { title: taskPatch.title.trim().slice(0, 80) || UNTITLED_THREAD } : {}) });
|
|
1819
2293
|
// Older threads may still inherit settings. Freeze their effective
|
|
1820
2294
|
// values before updating the default so "other threads unchanged" also
|
|
1821
2295
|
// holds for workspaces created before per-thread approval settings.
|
|
@@ -1835,6 +2309,20 @@ export class Store {
|
|
|
1835
2309
|
this.emit({ type: "bot", botId });
|
|
1836
2310
|
return task;
|
|
1837
2311
|
}
|
|
2312
|
+
/** One durable write: never leave only part of a bot's threads updated. */
|
|
2313
|
+
setAllThreadApprovalMode(botId, mode) {
|
|
2314
|
+
const bot = this.bot(botId);
|
|
2315
|
+
if (!bot)
|
|
2316
|
+
return null;
|
|
2317
|
+
const patch = { approvalMode: mode, autoApprove: false, alwaysAllow: [] };
|
|
2318
|
+
const tasks = (bot.tasks ?? []).map(task => ({ ...task, ...patch }));
|
|
2319
|
+
const next = { ...bot, ...patch, approvalGrant: undefined, tasks };
|
|
2320
|
+
this.saveBots(this.bots.map(candidate => candidate === bot ? next : candidate));
|
|
2321
|
+
bot.tasks?.forEach((task, index) => Object.assign(task, tasks[index]));
|
|
2322
|
+
Object.assign(bot, patch, { approvalGrant: undefined });
|
|
2323
|
+
this.emit({ type: "bot", botId });
|
|
2324
|
+
return bot;
|
|
2325
|
+
}
|
|
1838
2326
|
mirrorActiveTask(bot, task) {
|
|
1839
2327
|
bot.threadId = task.threadId;
|
|
1840
2328
|
bot.resumeCursors = structuredClone(task.resumeCursors);
|
|
@@ -1843,23 +2331,25 @@ export class Store {
|
|
|
1843
2331
|
}
|
|
1844
2332
|
/** A fresh context on the same bot: new thread, new session, same
|
|
1845
2333
|
* persona/tools/computer. Becomes the active task. */
|
|
1846
|
-
createTask(botId, title, activate = true, projectId, openedBy) {
|
|
2334
|
+
createTask(botId, title, activate = true, projectId, openedBy, approvalMode) {
|
|
1847
2335
|
const bot = this.bot(botId);
|
|
1848
2336
|
if (!bot)
|
|
1849
2337
|
return null;
|
|
1850
2338
|
if (projectId !== undefined && !this.project(botId, projectId))
|
|
1851
2339
|
return null;
|
|
2340
|
+
const createdAt = Date.now();
|
|
1852
2341
|
const task = {
|
|
1853
2342
|
threadId: newId(),
|
|
1854
2343
|
title: threadTitleFrom(title),
|
|
1855
|
-
createdAt
|
|
2344
|
+
createdAt,
|
|
2345
|
+
updatedAt: createdAt,
|
|
1856
2346
|
...(projectId ? { projectId } : {}),
|
|
1857
2347
|
...(openedBy ? { openedBy: structuredClone(openedBy) } : {}),
|
|
1858
2348
|
resumeCursors: {},
|
|
1859
2349
|
modelSelection: structuredClone(bot.modelSelection),
|
|
1860
|
-
approvalMode: approvalModeFor(bot),
|
|
1861
|
-
autoApprove: Boolean(bot.autoApprove),
|
|
1862
|
-
alwaysAllow: [...(bot.alwaysAllow ?? [])],
|
|
2350
|
+
approvalMode: approvalMode ?? approvalModeFor(bot),
|
|
2351
|
+
autoApprove: approvalMode ? false : Boolean(bot.autoApprove),
|
|
2352
|
+
alwaysAllow: approvalMode ? [] : [...(bot.alwaysAllow ?? [])],
|
|
1863
2353
|
unread: false,
|
|
1864
2354
|
activity: "idle",
|
|
1865
2355
|
busy: false,
|
|
@@ -1932,12 +2422,29 @@ export class Store {
|
|
|
1932
2422
|
* interleave in one transcript. `label` names that thread; the caller
|
|
1933
2423
|
* closes it once its result has been reported. A pair conversation
|
|
1934
2424
|
* never auto-closes. */
|
|
2425
|
+
/** The identity a peer-opened row belongs to: its opener's id while that
|
|
2426
|
+
* bot lives, else the one live bot the stamp's name still points at —
|
|
2427
|
+
* what a deleted-and-recreated same-name bot inherits — else the dead id
|
|
2428
|
+
* itself. A name two live bots share resolves to nobody's twin:
|
|
2429
|
+
* ambiguous means unmatched, never a wrong merge. */
|
|
2430
|
+
openerIdentity(openedBy) {
|
|
2431
|
+
if (this.bot(openedBy.botId))
|
|
2432
|
+
return openedBy.botId;
|
|
2433
|
+
const named = this.bots.filter((bot) => bot.name === openedBy.name);
|
|
2434
|
+
return named.length === 1 ? named[0].id : openedBy.botId;
|
|
2435
|
+
}
|
|
1935
2436
|
resolvePairConversation(sender, recipientId, options) {
|
|
1936
2437
|
if (!this.bot(recipientId))
|
|
1937
2438
|
return null;
|
|
1938
2439
|
const title = `@${sender.name}`;
|
|
1939
2440
|
const opener = (kind, at = Date.now()) => ({ botId: sender.id, name: sender.name, kind, at });
|
|
1940
|
-
|
|
2441
|
+
// Identity-stable: the sender's own rows, plus ones a deleted
|
|
2442
|
+
// predecessor opened when the stamp's name still points at exactly
|
|
2443
|
+
// this bot — a same-name recreation inherits the conversation instead
|
|
2444
|
+
// of minting a twin while the old row dangles live. A name two live
|
|
2445
|
+
// bots share matches nobody's inheritance: refusing the fallback can
|
|
2446
|
+
// cost a new row, never merge two bots' histories.
|
|
2447
|
+
const fromSender = this.tasks(recipientId).filter((task) => task.openedBy && this.openerIdentity(task.openedBy) === sender.id);
|
|
1941
2448
|
let pair = fromSender.find((task) => task.openedBy?.kind === "pair");
|
|
1942
2449
|
if (!pair) {
|
|
1943
2450
|
const lastActivity = (task) => this.messagesTail(task.threadId, 1).messages.at(-1)?.at ?? task.openedBy?.at ?? task.createdAt;
|
|
@@ -1966,6 +2473,14 @@ export class Store {
|
|
|
1966
2473
|
// back up, never replaced: closing is only the sidebar's idle state.
|
|
1967
2474
|
if (pair.closedBy)
|
|
1968
2475
|
this.setTaskClosedBy(recipientId, pair.threadId, null);
|
|
2476
|
+
// An inherited row carries the predecessor's id; rebind it to the
|
|
2477
|
+
// live bot so the name fallback is needed only once — a namesake
|
|
2478
|
+
// appearing later cannot claim the row. The hour it was opened
|
|
2479
|
+
// stays: inheritance is not a new conversation.
|
|
2480
|
+
const inherited = pair.openedBy;
|
|
2481
|
+
if (inherited && inherited.botId !== sender.id) {
|
|
2482
|
+
this.setTaskOpenedBy(recipientId, pair.threadId, { ...inherited, botId: sender.id, name: sender.name });
|
|
2483
|
+
}
|
|
1969
2484
|
return { task: pair, created: false };
|
|
1970
2485
|
}
|
|
1971
2486
|
// The brief is never a title. An 80-character slice of an assignment
|