openmausbot 0.1.84 → 0.1.86
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/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-DpUZNnFD.js +1 -0
- package/dist/assets/arc-DW7mrI4p.js +1 -0
- package/dist/assets/architectureDiagram-NJMV4G6O-CyeZ5WLZ.js +36 -0
- package/dist/assets/blockDiagram-BEXU5L5S-CBVaNjO7.js +140 -0
- package/dist/assets/c4Diagram-YGBWAQC7-sf8d41uT.js +38 -0
- package/dist/assets/channel-BXFkgus9.js +1 -0
- package/dist/assets/chunk-2Q5K7J3B-BOGvy7Y_.js +1 -0
- package/dist/assets/chunk-3FUC2YCW-BiF9zyiz.js +2 -0
- package/dist/assets/chunk-5VM5RSS4-EFuZ1SBZ.js +15 -0
- package/dist/assets/chunk-GTNCS2PH-pJLNSAT7.js +88 -0
- package/dist/assets/chunk-GWA4HPMP-Cb-3_3IT.js +1 -0
- package/dist/assets/chunk-JWPE2WC7-DAf5aogn.js +1 -0
- package/dist/assets/chunk-NETBCI7D-DR3kiwZz.js +1 -0
- package/dist/assets/chunk-WEXAMYUT-BmU8PeyM.js +1 -0
- package/dist/assets/chunk-XXDRQBXY-Buh8g8OT.js +1 -0
- package/dist/assets/classDiagram-v2-NBCMYWYE-BurfmMWM.js +217 -0
- package/dist/assets/cose-bilkent-JH36ORCC-KkGZUEkU.js +1 -0
- package/dist/assets/cynefin-OW5HDTMX-C3PpV96d.js +166 -0
- package/dist/assets/cynefinDiagram-VND7K2PF-Dt8JwJaI.js +62 -0
- package/dist/assets/cytoscape.esm-BB4DxJjf.js +321 -0
- package/dist/assets/dagre-6A5THRUB-DkjN8266.js +4 -0
- package/dist/assets/defaultLocale-DX6XiGOO.js +1 -0
- package/dist/assets/diagram-22UHCM2B-DUAJS0JV.js +200 -0
- package/dist/assets/diagram-3UASUU5V-Dw4KhF1a.js +24 -0
- package/dist/assets/diagram-ATOU4E4O-Dd1gmQqf.js +3 -0
- package/dist/assets/diagram-CDSNMT55-CjwGs0Yw.js +30 -0
- package/dist/assets/diagram-MLGK6HIB-BL-69Zrq.js +24 -0
- package/dist/assets/diagram-MPIPVDR6-5Zi2N7wo.js +41 -0
- package/dist/assets/ebnfDiagram-ZINNZB2B-D9NekWUX.js +1 -0
- package/dist/assets/elk-276RUBZZ-CkaSQjxw.js +27 -0
- package/dist/assets/erDiagram-OPXOYQCR-CdAVBV8-.js +99 -0
- package/dist/assets/flowDiagram-KWPJA3E3-Bm_lWWH7.js +213 -0
- package/dist/assets/ganttDiagram-FUAMR5RP-FZ8ejYWJ.js +292 -0
- package/dist/assets/gitGraphDiagram-X574FWY7-Dlv9rizZ.js +106 -0
- package/dist/assets/graph-D8UmR1vK.js +1 -0
- package/dist/assets/index-BGgqlWPJ.css +1 -0
- package/dist/assets/index-BtHo8ZKl.js +577 -0
- package/dist/assets/{index-DNa2umw-.js → index-h9FFNwTu.js} +1 -1
- package/dist/assets/infoDiagram-VRGFBTTK-BdQ_gW5B.js +2 -0
- package/dist/assets/init-Gi6I4Gst.js +1 -0
- package/dist/assets/ishikawaDiagram-OU5B5YK6-EwssEp2X.js +70 -0
- package/dist/assets/journeyDiagram-ZHPQQLJL-BX1G1vOd.js +139 -0
- package/dist/assets/kanban-definition-PNTS6WVX-BB4kAsyC.js +89 -0
- package/dist/assets/linear-3su2TG93.js +1 -0
- package/dist/assets/mermaid.core-CDD1E0s9.js +311 -0
- package/dist/assets/mindmap-definition-NLK3R4M7-Ba50MsNK.js +96 -0
- package/dist/assets/ordinal-Cboi1Yqb.js +1 -0
- package/dist/assets/pegDiagram-GJSIUBJH-DZLkkZlM.js +1 -0
- package/dist/assets/pieDiagram-5QR66LMP-DaTz77Uq.js +39 -0
- package/dist/assets/quadrantDiagram-O4NWA36T-CpaiidpR.js +7 -0
- package/dist/assets/railroadDiagram-XR7U4H2S-mN4JrMeI.js +1 -0
- package/dist/assets/requirementDiagram-PLB6GJNP-BSNDRpg7.js +84 -0
- package/dist/assets/sankeyDiagram-IPEJSGJF-FngzGNug.js +40 -0
- package/dist/assets/sequenceDiagram-PO4LG4MO-CM4pCC3L.js +169 -0
- package/dist/assets/sizeCapture-INFHLROL-CRICHXkw.js +1 -0
- package/dist/assets/stateDiagram-v2-GCMORJYK-oyTWpxH2.js +291 -0
- package/dist/assets/swimlanes-2SLR337P-D9h9mPBq.js +1 -0
- package/dist/assets/swimlanesDiagram-TC7HE7FX-BWrvB2ak.js +8 -0
- package/dist/assets/timeline-definition-EJHVYXUP-BJUlN9bW.js +120 -0
- package/dist/assets/usecaseDiagram-POWQR4AR-3uBNkbAb.js +370 -0
- package/dist/assets/vennDiagram-UO4OBE2U-D2odalzT.js +34 -0
- package/dist/assets/wardleyDiagram-VNRHLVJA-DHgFaB9S.js +78 -0
- package/dist/assets/xychartDiagram-PMCCYNJV-w5GYM2h0.js +7 -0
- package/dist/index.html +2 -2
- package/dist-server/companion/src/routes.js +1 -0
- package/dist-server/container-mcp.js +231 -17
- package/dist-server/drivers/agents-proxy.js +16097 -1411
- package/dist-server/enterprise/LICENSE +32 -0
- package/dist-server/enterprise/server/index.js +6 -5
- package/dist-server/hooks/omb-hook.js +74 -0
- package/dist-server/index.js +12495 -7584
- package/dist-server/local-computer-proxy.js +83 -1
- package/dist-server/local-computer.js +14 -2
- package/dist-server/mcp-server.js +3 -1
- package/dist-server/openmausbot.js +5458 -4587
- package/dist-server/pair-cli.js +5458 -4587
- package/dist-server/proxy-paths.js +1 -0
- package/dist-server/server/admin-activity.js +410 -0
- package/dist-server/server/agent-tool-policy.js +1 -0
- package/dist-server/server/bot-package.js +2 -0
- package/dist-server/server/bot-visibility.js +462 -0
- package/dist-server/server/box.js +25 -5
- package/dist-server/server/browser-engine.js +87 -10
- package/dist-server/server/browser-live.js +7 -5
- package/dist-server/server/browser-runtime.js +66 -11
- package/dist-server/server/channel-queue.js +2 -0
- package/dist-server/server/checkpoints.js +62 -18
- package/dist-server/server/cli-prompts.js +26 -2
- package/dist-server/server/cli.js +79 -10
- package/dist-server/server/commands.js +27 -0
- package/dist-server/server/compaction-summary.js +78 -0
- package/dist-server/server/composio.js +49 -11
- package/dist-server/server/config.js +137 -14
- package/dist-server/server/context-budget.js +29 -0
- package/dist-server/server/context-rebuild.js +72 -0
- package/dist-server/server/decision-log.js +209 -48
- package/dist-server/server/default-model-selection.js +20 -3
- package/dist-server/server/delegations.js +11 -9
- package/dist-server/server/digest.js +130 -0
- package/dist-server/server/drivers/acp/core.js +637 -238
- package/dist-server/server/drivers/acp/grok.js +4 -3
- package/dist-server/server/drivers/agents-call.js +1123 -0
- package/dist-server/server/drivers/agents-catalog.js +659 -0
- package/dist-server/server/drivers/agents-client.js +28 -0
- package/dist-server/server/drivers/agents-proxy.js +18 -1683
- package/dist-server/server/drivers/agents-result.js +23 -0
- package/dist-server/server/drivers/chat-mcp-tools.js +17 -2
- package/dist-server/server/drivers/claude.js +83 -16
- package/dist-server/server/drivers/codex-mcp-names.js +44 -0
- package/dist-server/server/drivers/codex.js +32 -4
- package/dist-server/server/drivers/grok.js +2 -1
- package/dist-server/server/drivers/openai-chat.js +66 -45
- package/dist-server/server/drivers/openai-compat.js +16 -8
- package/dist-server/server/enterprise.js +97 -16
- package/dist-server/server/external-runtime.js +83 -0
- package/dist-server/server/guarded-requests.js +60 -0
- package/dist-server/server/harness/http.js +61 -0
- package/dist-server/server/hooks/omb-hook.js +103 -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 +2810 -678
- package/dist-server/server/local-computer.js +21 -2
- 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 +10 -2
- package/dist-server/server/mcp-tool-images.js +37 -0
- package/dist-server/server/mcp-tool-schema.js +108 -0
- package/dist-server/server/message-db.js +108 -21
- package/dist-server/server/message-file.js +3 -2
- package/dist-server/server/model-context-window.js +7 -0
- package/dist-server/server/model-prices.js +121 -0
- package/dist-server/server/notify.js +10 -1
- package/dist-server/server/package-export.js +1 -0
- package/dist-server/server/peer-roster.js +17 -4
- package/dist-server/server/provider-icon.js +20 -0
- package/dist-server/server/proxy-paths.js +1 -0
- package/dist-server/server/recent-work.js +15 -0
- package/dist-server/server/request-auth.js +97 -0
- package/dist-server/server/room-handoffs.js +41 -13
- 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 +39 -1
- package/dist-server/server/routines.js +65 -9
- package/dist-server/server/shared-computer-control.js +31 -4
- package/dist-server/server/spend.js +111 -16
- package/dist-server/server/steer-queue.js +12 -1
- package/dist-server/server/store.js +384 -33
- package/dist-server/server/system-prompt.js +4 -4
- package/dist-server/server/team-backup.js +19 -2
- package/dist-server/server/thread-starters.js +53 -0
- package/dist-server/server/tool-results.js +72 -0
- package/dist-server/server/tts/grok.js +74 -0
- package/dist-server/server/tts/index.js +19 -1
- package/dist-server/server/usage-ledger.js +31 -8
- package/dist-server/server/webhooks.js +28 -0
- package/dist-server/server/workspace-backup-policy.js +9 -2
- package/dist-server/server/workspace-backup.js +7 -4
- package/dist-server/shared/approval-mode.js +7 -1
- package/dist-server/shared/digest.js +1 -0
- package/dist-server/shared/markdown-windows-paths.js +36 -0
- package/dist-server/shared/mention-boundary.js +15 -0
- package/dist-server/shared/provider-icon.js +110 -0
- package/dist-server/shared/routines.js +7 -1
- package/dist-server/shared/team-backup.js +5 -0
- package/dist-server/src/lib/composer-attachments.js +16 -0
- package/dist-server/vps-container-mcp.js +236 -22
- package/enterprise/README.md +33 -12
- package/enterprise/server/index.js +6 -5
- package/package.json +1 -1
- package/dist/assets/index-BbU5REzd.js +0 -310
- package/dist/assets/index-CLGfYlx_.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";
|
|
@@ -11,12 +11,14 @@ import { removeBotFolder, soulFile, soulHash, writeSoulMirror } from "./bot-fold
|
|
|
11
11
|
import { peerAllowKey } from "./peer-approval-key.js";
|
|
12
12
|
import { DATA_DIR, EVENTS_DIR, NATIVE_DIR, loadBrowserProfileIdAliases } from "./config.js";
|
|
13
13
|
import * as mdb from "./message-db.js";
|
|
14
|
+
import { runCommand } from "./commands.js";
|
|
14
15
|
import { workspaceDir } from "./workspace.js";
|
|
15
16
|
import { newId } from "./contracts.js";
|
|
16
17
|
import { pickBotName } from "./names.js";
|
|
17
18
|
import { redactSecretsInText } from "./redact.js";
|
|
18
19
|
import { botAvatarProfile } from "../shared/bot-avatar.js";
|
|
19
20
|
import { approvalModeFor, isApprovalMode } from "../shared/approval-mode.js";
|
|
21
|
+
import { isMentionBoundary, isMentionNameContinuation } from "../shared/mention-boundary.js";
|
|
20
22
|
export const groupWireProjectionIsExact = true;
|
|
21
23
|
// Unicode's complete emoji sequences include flags, skin tones and ZWJ
|
|
22
24
|
// combinations. Also allow unqualified single symbols (e.g. ♥), but not
|
|
@@ -29,14 +31,23 @@ export const taskWireProjectionIsExact = true;
|
|
|
29
31
|
/** The typed wire projection for one task. Pairs with the assertion above:
|
|
30
32
|
* returning WireTask means an undeclared server field cannot ride silently. */
|
|
31
33
|
export function toWireTask(task) {
|
|
32
|
-
const { resumeCursors: _resumeCursors, lastInstanceId: _lastInstanceId, handedMessages: _handedMessages, ...wire } = task;
|
|
34
|
+
const { resumeCursors: _resumeCursors, lastInstanceId: _lastInstanceId, handedMessages: _handedMessages, appliedCompactionId: _appliedCompactionId, contextFloor: _contextFloor, lastContextModel: _lastContextModel, ...wire } = task;
|
|
33
35
|
return wire;
|
|
34
36
|
}
|
|
35
37
|
const TASK_PATCH_FIELDS = [
|
|
36
38
|
"title", "projectId", "modelSelection", "approvalMode", "autoApprove", "alwaysAllow",
|
|
37
|
-
"unread", "rewound", "archivedAt", "pinnedMessageId", "resumeCursors", "lastInstanceId", "cwd",
|
|
38
|
-
"routineRunId", "surface",
|
|
39
|
+
"unread", "rewound", "archivedAt", "pinned", "pinnedMessageId", "resumeCursors", "lastInstanceId", "cwd",
|
|
40
|
+
"routineRunId", "surface", "appliedCompactionId", "contextFloor", "lastContextModel",
|
|
39
41
|
];
|
|
42
|
+
/** Only `true` is a pin. false/undefined must not land in bots.json or groups.json. */
|
|
43
|
+
function persistedPin(task) {
|
|
44
|
+
if (task.pinned === true)
|
|
45
|
+
return task;
|
|
46
|
+
if (!("pinned" in task))
|
|
47
|
+
return task;
|
|
48
|
+
const { pinned: _pinned, ...rest } = task;
|
|
49
|
+
return rest;
|
|
50
|
+
}
|
|
40
51
|
/** Everything the BOT authored is scrubbed of content-shaped secrets before
|
|
41
52
|
* it is stored: its reply text, a tool title (an ACP engine's title can be
|
|
42
53
|
* the whole command line) and the command beside it, a permission card's
|
|
@@ -50,6 +61,8 @@ function redactBotAuthored(message) {
|
|
|
50
61
|
const out = { ...message };
|
|
51
62
|
if (typeof out.text === "string")
|
|
52
63
|
out.text = redactSecretsInText(out.text);
|
|
64
|
+
if (out.compaction)
|
|
65
|
+
out.compaction = { ...out.compaction, summary: redactSecretsInText(out.compaction.summary) };
|
|
53
66
|
if (out.tool?.name) {
|
|
54
67
|
out.tool = { ...out.tool, name: redactSecretsInText(out.tool.name) };
|
|
55
68
|
if (out.tool.summary)
|
|
@@ -227,9 +240,46 @@ export function titleFromMessage(text) {
|
|
|
227
240
|
const line = text.trim().split("\n")[0].trim();
|
|
228
241
|
return line.length > 48 ? `${line.slice(0, 47)}…` : line || UNTITLED_TASK;
|
|
229
242
|
}
|
|
243
|
+
/** One usable line out of a model's title reply: the first line, no
|
|
244
|
+
* surrounding quotes, code fences, or markdown decoration, no trailing
|
|
245
|
+
* period, single spaces — or null when what came back is empty, too long
|
|
246
|
+
* to be a title, or otherwise not a plain name. The caller keeps its
|
|
247
|
+
* fallback then. */
|
|
248
|
+
export function titleFromLlm(raw) {
|
|
249
|
+
const line = raw
|
|
250
|
+
.trim()
|
|
251
|
+
.split("\n")[0]
|
|
252
|
+
.replace(/^[#*\-\u2022]+/, "")
|
|
253
|
+
.replace(/^["'\u201C\u201D\u2018\u2019\u0060]+/, "")
|
|
254
|
+
.replace(/["'\u201C\u201D\u2018\u2019\u0060]+$/, "")
|
|
255
|
+
// decoration the quotes were hiding: "## Deploy app" keeps its
|
|
256
|
+
// markers through the strips above, which never reach past a quote
|
|
257
|
+
.replace(/^[#*\-\u2022]+/, "")
|
|
258
|
+
.replace(/[#*]+$/, "")
|
|
259
|
+
.replace(/[.\u3002]+$/, "")
|
|
260
|
+
.replace(/\s+/g, " ")
|
|
261
|
+
.trim();
|
|
262
|
+
return line.length >= 1 && line.length <= 48 ? line : null;
|
|
263
|
+
}
|
|
230
264
|
export const botWireProjectionIsExact = true;
|
|
231
265
|
const BOTS_FILE = join(DATA_DIR, "bots.json");
|
|
232
266
|
const GROUPS_FILE = join(DATA_DIR, "groups.json");
|
|
267
|
+
/** The registries carry souls, project paths and per-bot settings, so they are
|
|
268
|
+
* owner-only like the other data-dir stores. A file written by an older
|
|
269
|
+
* release (or loosened by hand) is tightened on load. Best effort: failing to
|
|
270
|
+
* tighten must never stop the fleet from loading, and the next save replaces
|
|
271
|
+
* the file with a 0600 one anyway. Windows has no POSIX mode bits. */
|
|
272
|
+
function tightenRegistryFile(file) {
|
|
273
|
+
if (process.platform === "win32")
|
|
274
|
+
return;
|
|
275
|
+
try {
|
|
276
|
+
if ((statSync(file).mode & 0o077) !== 0)
|
|
277
|
+
chmodSync(file, 0o600);
|
|
278
|
+
}
|
|
279
|
+
catch {
|
|
280
|
+
/* absent, or not ours to change */
|
|
281
|
+
}
|
|
282
|
+
}
|
|
233
283
|
const messagesFile = (threadId) => join(DATA_DIR, `messages-${threadId}.json`);
|
|
234
284
|
const COLORS = [
|
|
235
285
|
"green",
|
|
@@ -259,15 +309,15 @@ export function mentionedBots(text, peers) {
|
|
|
259
309
|
const found = [];
|
|
260
310
|
let at = -1;
|
|
261
311
|
while ((at = lower.indexOf("@", at + 1)) !== -1) {
|
|
262
|
-
if (
|
|
312
|
+
if (!isMentionBoundary(text, at))
|
|
263
313
|
continue; // user@host, not a tag
|
|
264
314
|
const rest = lower.slice(at + 1);
|
|
265
315
|
const hit = candidates.find((p) => {
|
|
266
316
|
const name = p.name.toLowerCase();
|
|
267
317
|
if (!rest.startsWith(name))
|
|
268
318
|
return false;
|
|
269
|
-
const after = rest
|
|
270
|
-
return
|
|
319
|
+
const after = rest.slice(name.length); // must not run into a longer word
|
|
320
|
+
return !isMentionNameContinuation(after);
|
|
271
321
|
});
|
|
272
322
|
if (hit && !found.includes(hit))
|
|
273
323
|
found.push(hit);
|
|
@@ -300,8 +350,15 @@ export function normalizeGroupDefaultResponder(value, memberIds, dm = false) {
|
|
|
300
350
|
* otherwise the room policy chooses one member, everyone, or nobody. */
|
|
301
351
|
export function roomResponders(text, members, defaultResponder) {
|
|
302
352
|
const available = members.filter((member) => !member.hidden);
|
|
303
|
-
|
|
304
|
-
|
|
353
|
+
const everyone = "everyone";
|
|
354
|
+
for (let at = text.indexOf("@"); at !== -1; at = text.indexOf("@", at + 1)) {
|
|
355
|
+
const candidate = text.slice(at + 1, at + 1 + everyone.length);
|
|
356
|
+
if (isMentionBoundary(text, at) &&
|
|
357
|
+
candidate.toLocaleLowerCase() === everyone &&
|
|
358
|
+
!isMentionNameContinuation(text.slice(at + 1 + everyone.length))) {
|
|
359
|
+
return available;
|
|
360
|
+
}
|
|
361
|
+
}
|
|
305
362
|
const mentioned = mentionedBots(text, available);
|
|
306
363
|
if (mentioned.length)
|
|
307
364
|
return mentioned;
|
|
@@ -327,6 +384,8 @@ export class Store {
|
|
|
327
384
|
constructor(defaultSelection) {
|
|
328
385
|
this.defaultSelection = defaultSelection;
|
|
329
386
|
mkdirSync(DATA_DIR, { recursive: true });
|
|
387
|
+
for (const file of [BOTS_FILE, GROUPS_FILE])
|
|
388
|
+
tightenRegistryFile(file);
|
|
330
389
|
try {
|
|
331
390
|
this.bots = JSON.parse(readFileSync(BOTS_FILE, "utf8"));
|
|
332
391
|
}
|
|
@@ -475,6 +534,7 @@ export class Store {
|
|
|
475
534
|
}
|
|
476
535
|
for (const g of this.groups) {
|
|
477
536
|
g.busyBotId = null;
|
|
537
|
+
delete g.turnStartedAt;
|
|
478
538
|
const normalized = normalizeGroupDefaultResponder(g.defaultResponder, g.memberIds, Boolean(g.dm));
|
|
479
539
|
if (JSON.stringify(normalized) !== JSON.stringify(g.defaultResponder))
|
|
480
540
|
groupsMigrated = true;
|
|
@@ -492,6 +552,7 @@ export class Store {
|
|
|
492
552
|
threadId: g.threadId,
|
|
493
553
|
title: this.firstUserLine(g.threadId) ?? UNTITLED_TASK,
|
|
494
554
|
createdAt: g.createdAt,
|
|
555
|
+
updatedAt: g.createdAt,
|
|
495
556
|
};
|
|
496
557
|
if (g.pinnedCwd !== undefined)
|
|
497
558
|
initialTask.pinnedCwd = g.pinnedCwd;
|
|
@@ -527,6 +588,7 @@ export class Store {
|
|
|
527
588
|
threadId: b.threadId,
|
|
528
589
|
title: this.firstUserLine(b.threadId) ?? UNTITLED_TASK,
|
|
529
590
|
createdAt: b.createdAt,
|
|
591
|
+
updatedAt: b.createdAt,
|
|
530
592
|
resumeCursors: b.resumeCursors ?? {},
|
|
531
593
|
}];
|
|
532
594
|
botsMigrated = true;
|
|
@@ -539,6 +601,7 @@ export class Store {
|
|
|
539
601
|
threadId: b.threadId,
|
|
540
602
|
title: this.firstUserLine(b.threadId) ?? UNTITLED_TASK,
|
|
541
603
|
createdAt: b.createdAt,
|
|
604
|
+
updatedAt: b.createdAt,
|
|
542
605
|
resumeCursors: b.resumeCursors ?? {},
|
|
543
606
|
};
|
|
544
607
|
b.tasks.unshift(active);
|
|
@@ -571,10 +634,11 @@ export class Store {
|
|
|
571
634
|
delete task.approvalMode;
|
|
572
635
|
botsMigrated = true;
|
|
573
636
|
}
|
|
574
|
-
if (task.busy !== undefined || task.activity !== undefined)
|
|
637
|
+
if (task.busy !== undefined || task.activity !== undefined || task.turnStartedAt !== undefined)
|
|
575
638
|
botsMigrated = true;
|
|
576
639
|
task.busy = false;
|
|
577
640
|
task.activity = "idle";
|
|
641
|
+
task.turnStartedAt = undefined;
|
|
578
642
|
}
|
|
579
643
|
this.mirrorActiveTask(b, active);
|
|
580
644
|
b.unread = b.tasks.some((task) => task.unread);
|
|
@@ -593,18 +657,124 @@ export class Store {
|
|
|
593
657
|
if (existsSync(legacyFile))
|
|
594
658
|
mdb.readThread(threadId, legacyFile);
|
|
595
659
|
}
|
|
660
|
+
// After legacy transcripts are in SQLite, so the first boot sees their
|
|
661
|
+
// newest message instead of stamping createdAt and jumping next launch.
|
|
662
|
+
this.repairThreadUpdatedAts();
|
|
663
|
+
// After the roster is loaded, so identity resolution sees which opener
|
|
664
|
+
// ids are still live.
|
|
665
|
+
this.repairDuplicatePairConversations();
|
|
596
666
|
this.registeringInitialSections = false;
|
|
597
667
|
}
|
|
668
|
+
/** Advance a task's update stamp in memory only. Message writes must not
|
|
669
|
+
* rewrite bots.json; the next snapshot and the startup repair read this. */
|
|
670
|
+
noteThreadActivity(threadId, at) {
|
|
671
|
+
if (!Number.isFinite(at))
|
|
672
|
+
return;
|
|
673
|
+
const advance = (current) => Math.max(current ?? 0, at);
|
|
674
|
+
for (const bot of this.bots) {
|
|
675
|
+
const task = bot.tasks?.find((candidate) => candidate.threadId === threadId);
|
|
676
|
+
if (!task)
|
|
677
|
+
continue;
|
|
678
|
+
const next = advance(task.updatedAt);
|
|
679
|
+
if (task.updatedAt !== next)
|
|
680
|
+
task.updatedAt = next;
|
|
681
|
+
}
|
|
682
|
+
for (const group of this.groups) {
|
|
683
|
+
const task = group.tasks?.find((candidate) => candidate.threadId === threadId);
|
|
684
|
+
if (!task)
|
|
685
|
+
continue;
|
|
686
|
+
const next = advance(task.updatedAt);
|
|
687
|
+
if (task.updatedAt !== next)
|
|
688
|
+
task.updatedAt = next;
|
|
689
|
+
}
|
|
690
|
+
}
|
|
691
|
+
/** Fill missing stamps, and raise a stamp that is older than the newest
|
|
692
|
+
* stored message. Never moves a stamp backwards: there is no per-message
|
|
693
|
+
* delete, and a clock skew must not reshuffle the list on every boot. */
|
|
694
|
+
repairThreadUpdatedAts() {
|
|
695
|
+
const botTasks = this.bots.flatMap((bot) => bot.tasks ?? []);
|
|
696
|
+
const groupTasks = this.groups.flatMap((group) => group.tasks ?? []);
|
|
697
|
+
const latest = mdb.latestMessageAts([...botTasks, ...groupTasks].map((task) => task.threadId));
|
|
698
|
+
let botsDirty = false;
|
|
699
|
+
let groupsDirty = false;
|
|
700
|
+
const repair = (task) => {
|
|
701
|
+
const fromMessages = latest.get(task.threadId);
|
|
702
|
+
const next = fromMessages !== undefined && (task.updatedAt === undefined || fromMessages > task.updatedAt)
|
|
703
|
+
? fromMessages
|
|
704
|
+
: task.updatedAt ?? task.createdAt;
|
|
705
|
+
if (task.updatedAt === next)
|
|
706
|
+
return false;
|
|
707
|
+
task.updatedAt = next;
|
|
708
|
+
return true;
|
|
709
|
+
};
|
|
710
|
+
for (const task of botTasks)
|
|
711
|
+
if (repair(task))
|
|
712
|
+
botsDirty = true;
|
|
713
|
+
for (const task of groupTasks)
|
|
714
|
+
if (repair(task))
|
|
715
|
+
groupsDirty = true;
|
|
716
|
+
if (botsDirty)
|
|
717
|
+
this.saveBots();
|
|
718
|
+
if (groupsDirty)
|
|
719
|
+
this.saveGroups();
|
|
720
|
+
}
|
|
721
|
+
/** At most one live pair row per (recipient, sender identity). Servers
|
|
722
|
+
* before identity-stable matching minted a second live pair row for a
|
|
723
|
+
* deleted-and-recreated sender while the predecessor's row dangled live
|
|
724
|
+
* forever; collapse those on load by keeping the row the resolver favors
|
|
725
|
+
* (first in the task list — the newest, actively used one) and demoting
|
|
726
|
+
* the rest to closed plain threads. History is kept, never deleted, and
|
|
727
|
+
* a demoted row is never re-adopted: adoption skips closed rows. Runs on
|
|
728
|
+
* every load and touches nothing in a store that already holds the
|
|
729
|
+
* invariant. */
|
|
730
|
+
repairDuplicatePairConversations() {
|
|
731
|
+
let botsDirty = false;
|
|
732
|
+
const sweep = (tasks, repaired) => {
|
|
733
|
+
const pairs = new Map();
|
|
734
|
+
for (const task of tasks ?? []) {
|
|
735
|
+
const by = task.openedBy;
|
|
736
|
+
if (by?.kind !== "pair" || task.closedBy)
|
|
737
|
+
continue;
|
|
738
|
+
const identity = this.openerIdentity(by);
|
|
739
|
+
pairs.set(identity, [...(pairs.get(identity) ?? []), task]);
|
|
740
|
+
}
|
|
741
|
+
for (const rows of pairs.values()) {
|
|
742
|
+
// rows are in task-list order, the same order the resolver's
|
|
743
|
+
// find() favors: the first is the one it keeps using.
|
|
744
|
+
for (const duplicate of rows.slice(1)) {
|
|
745
|
+
const by = duplicate.openedBy;
|
|
746
|
+
const identity = this.openerIdentity(by);
|
|
747
|
+
const { kind: _kind, ...opened } = by;
|
|
748
|
+
duplicate.openedBy = opened;
|
|
749
|
+
duplicate.closedBy = {
|
|
750
|
+
botId: identity,
|
|
751
|
+
name: this.bot(identity)?.name ?? by.name,
|
|
752
|
+
at: Date.now(),
|
|
753
|
+
};
|
|
754
|
+
repaired();
|
|
755
|
+
}
|
|
756
|
+
}
|
|
757
|
+
};
|
|
758
|
+
// Pair rows only ever live on bots: the resolver requires a bot
|
|
759
|
+
// recipient, and group tasks carry no peer stamps.
|
|
760
|
+
for (const bot of this.bots)
|
|
761
|
+
sweep(bot.tasks, () => { botsDirty = true; });
|
|
762
|
+
if (botsDirty)
|
|
763
|
+
this.saveBots();
|
|
764
|
+
}
|
|
598
765
|
saveBots(bots = this.bots) {
|
|
599
766
|
this.rememberSections([...this.bots, ...bots].map((bot) => bot.section));
|
|
600
767
|
writeFileAtomic(BOTS_FILE, JSON.stringify(bots.map(({ busy: _busy, activity: _activity, ...bot }) => ({
|
|
601
768
|
...bot,
|
|
602
|
-
tasks: bot.tasks?.map(({ busy: _taskBusy, activity: _taskActivity, ...task }) => task),
|
|
603
|
-
})), null, 2));
|
|
769
|
+
tasks: bot.tasks?.map(({ busy: _taskBusy, activity: _taskActivity, turnStartedAt: _taskTurnStarted, ...task }) => persistedPin(task)),
|
|
770
|
+
})), null, 2), { mode: 0o600 });
|
|
604
771
|
}
|
|
605
772
|
saveGroups() {
|
|
606
773
|
this.rememberSections(this.groups.map((group) => group.section));
|
|
607
|
-
writeFileAtomic(GROUPS_FILE, JSON.stringify(this.groups.map(({ busyBotId: _busyBotId, ...g }) =>
|
|
774
|
+
writeFileAtomic(GROUPS_FILE, JSON.stringify(this.groups.map(({ busyBotId: _busyBotId, turnStartedAt: _turnStartedAt, ...g }) => ({
|
|
775
|
+
...g,
|
|
776
|
+
...(g.tasks ? { tasks: g.tasks.map((task) => persistedPin(task)) } : {}),
|
|
777
|
+
})), null, 2), { mode: 0o600 });
|
|
608
778
|
}
|
|
609
779
|
get sections() { return readSections(); }
|
|
610
780
|
rememberSections(names) {
|
|
@@ -689,7 +859,7 @@ export class Store {
|
|
|
689
859
|
section,
|
|
690
860
|
};
|
|
691
861
|
if (!dm) {
|
|
692
|
-
group.tasks = [{ threadId, title: UNTITLED_TASK, createdAt }];
|
|
862
|
+
group.tasks = [{ threadId, title: UNTITLED_TASK, createdAt, updatedAt: createdAt }];
|
|
693
863
|
group.setupCompletedAt = setup?.completed ? createdAt : null;
|
|
694
864
|
group.setupSkippedAt = null;
|
|
695
865
|
}
|
|
@@ -709,7 +879,19 @@ export class Store {
|
|
|
709
879
|
if (Object.prototype.hasOwnProperty.call(patch, "section")) {
|
|
710
880
|
this.rememberSections([patch.section]);
|
|
711
881
|
}
|
|
882
|
+
const previousBusyBotId = group.busyBotId;
|
|
712
883
|
Object.assign(group, patch);
|
|
884
|
+
// The group's elapsed readout counts the busy member's turn from the
|
|
885
|
+
// claim time — the group-side twin of a task's turnStartedAt. Derived,
|
|
886
|
+
// never patched directly: stamp it on every transition into a busy
|
|
887
|
+
// speaker and clear it when the group goes idle, so each member's turn
|
|
888
|
+
// counts from its own start.
|
|
889
|
+
if (Object.prototype.hasOwnProperty.call(patch, "busyBotId")) {
|
|
890
|
+
if (patch.busyBotId && patch.busyBotId !== previousBusyBotId)
|
|
891
|
+
group.turnStartedAt = Date.now();
|
|
892
|
+
else if (!patch.busyBotId)
|
|
893
|
+
delete group.turnStartedAt;
|
|
894
|
+
}
|
|
713
895
|
if (!group.dm && Object.prototype.hasOwnProperty.call(patch, "pinnedMessageId")) {
|
|
714
896
|
const active = this.activeGroupTask(group.id);
|
|
715
897
|
if (active)
|
|
@@ -809,10 +991,12 @@ export class Store {
|
|
|
809
991
|
const group = this.group(groupId);
|
|
810
992
|
if (!group || group.dm)
|
|
811
993
|
return null;
|
|
994
|
+
const createdAt = Date.now();
|
|
812
995
|
const task = {
|
|
813
996
|
threadId: newId(),
|
|
814
997
|
title: title?.trim().slice(0, 80) || UNTITLED_TASK,
|
|
815
|
-
createdAt
|
|
998
|
+
createdAt,
|
|
999
|
+
updatedAt: createdAt,
|
|
816
1000
|
};
|
|
817
1001
|
group.tasks = [task, ...(group.tasks ?? [])];
|
|
818
1002
|
if (activate) {
|
|
@@ -836,6 +1020,18 @@ export class Store {
|
|
|
836
1020
|
this.emit({ type: "group", groupId });
|
|
837
1021
|
return group;
|
|
838
1022
|
}
|
|
1023
|
+
setGroupTaskPinned(groupId, threadId, pinned) {
|
|
1024
|
+
const task = this.groupTaskByThread(groupId, threadId);
|
|
1025
|
+
if (!task)
|
|
1026
|
+
return null;
|
|
1027
|
+
if (pinned)
|
|
1028
|
+
task.pinned = true;
|
|
1029
|
+
else
|
|
1030
|
+
delete task.pinned;
|
|
1031
|
+
this.saveGroups();
|
|
1032
|
+
this.emit({ type: "group", groupId });
|
|
1033
|
+
return task;
|
|
1034
|
+
}
|
|
839
1035
|
renameGroupTask(groupId, threadId, title) {
|
|
840
1036
|
const task = this.groupTaskByThread(groupId, threadId);
|
|
841
1037
|
if (!task)
|
|
@@ -845,13 +1041,27 @@ export class Store {
|
|
|
845
1041
|
this.emit({ type: "group", groupId });
|
|
846
1042
|
return task;
|
|
847
1043
|
}
|
|
1044
|
+
/** Name a channel task after its first message, once. Returns the task
|
|
1045
|
+
* it named so a caller can later replace exactly that machine-made
|
|
1046
|
+
* title. */
|
|
848
1047
|
titleGroupTaskFromFirstMessage(groupId, text, threadId) {
|
|
849
1048
|
const task = threadId ? this.groupTaskByThread(groupId, threadId) : this.activeGroupTask(groupId);
|
|
850
|
-
if (!task || task.title !== UNTITLED_TASK)
|
|
851
|
-
return;
|
|
1049
|
+
if (!task || task.titleFromFirstMessage || task.title !== UNTITLED_TASK)
|
|
1050
|
+
return null;
|
|
852
1051
|
task.title = titleFromMessage(text);
|
|
1052
|
+
task.titleFromFirstMessage = true;
|
|
853
1053
|
this.saveGroups();
|
|
854
1054
|
this.emit({ type: "group", groupId });
|
|
1055
|
+
return task;
|
|
1056
|
+
}
|
|
1057
|
+
/** Swap a machine-made first-message channel title for a generated one,
|
|
1058
|
+
* once, on the same snippet-equality contract as bot tasks: any rename
|
|
1059
|
+
* by the person breaks that equality first and always wins. */
|
|
1060
|
+
retitleGroupTask(groupId, threadId, machineTitle, title) {
|
|
1061
|
+
const task = this.groupTaskByThread(groupId, threadId);
|
|
1062
|
+
if (!task || task.title !== machineTitle)
|
|
1063
|
+
return null;
|
|
1064
|
+
return this.renameGroupTask(groupId, threadId, threadTitleFrom(title));
|
|
855
1065
|
}
|
|
856
1066
|
deleteGroupTask(groupId, threadId) {
|
|
857
1067
|
const group = this.group(groupId);
|
|
@@ -951,6 +1161,9 @@ export class Store {
|
|
|
951
1161
|
throw new Error("Cannot import over an existing conversation");
|
|
952
1162
|
mdb.importThread(threadId, messages, activeLeafId);
|
|
953
1163
|
this.threads.delete(threadId);
|
|
1164
|
+
const newest = messages.reduce((max, message) => Math.max(max, message.at), Number.NEGATIVE_INFINITY);
|
|
1165
|
+
if (Number.isFinite(newest))
|
|
1166
|
+
this.noteThreadActivity(threadId, newest);
|
|
954
1167
|
}
|
|
955
1168
|
activeLeaf(threadId) {
|
|
956
1169
|
return this.thread(threadId).activeLeafId;
|
|
@@ -982,18 +1195,24 @@ export class Store {
|
|
|
982
1195
|
}
|
|
983
1196
|
return null;
|
|
984
1197
|
}
|
|
985
|
-
appendMessage(threadId, message) {
|
|
1198
|
+
appendMessage(threadId, message, command) {
|
|
986
1199
|
const t = this.thread(threadId);
|
|
987
1200
|
const full = { id: newId(), at: Date.now(), parentId: t.activeLeafId, ...redactBotAuthored(message) };
|
|
1201
|
+
const persist = () => { mdb.appendMessage(threadId, full); return full; };
|
|
1202
|
+
const committed = command ? runCommand(command, persist) : persist();
|
|
1203
|
+
if (committed.id !== full.id)
|
|
1204
|
+
return committed;
|
|
1205
|
+
// Only publish the committed write. A failed receipt must leave both
|
|
1206
|
+
// the in-memory branch and subscribers unchanged, just like SQLite.
|
|
988
1207
|
t.messages.push(full);
|
|
989
1208
|
t.activeLeafId = full.id;
|
|
990
|
-
mdb.appendMessage(threadId, full);
|
|
991
1209
|
if (full.kind === "screen") {
|
|
992
1210
|
for (const pruned of this.pruneScreenFrames(t)) {
|
|
993
1211
|
mdb.updateMessage(threadId, pruned);
|
|
994
1212
|
this.emit({ type: "message.patch", threadId, message: pruned });
|
|
995
1213
|
}
|
|
996
1214
|
}
|
|
1215
|
+
this.noteThreadActivity(threadId, full.at);
|
|
997
1216
|
this.emit({ type: "message", threadId, message: full });
|
|
998
1217
|
// The first-run quiz is not a live ask. Talking past it hides it so the
|
|
999
1218
|
// transcript is just the greeting plus what they said. Cards with a
|
|
@@ -1024,6 +1243,7 @@ export class Store {
|
|
|
1024
1243
|
this.emit({ type: "message.patch", threadId, message: pruned });
|
|
1025
1244
|
}
|
|
1026
1245
|
}
|
|
1246
|
+
this.noteThreadActivity(threadId, full.at);
|
|
1027
1247
|
this.emit({ type: "message", threadId, message: full });
|
|
1028
1248
|
// announced after the insert so no client ever sees two siblings
|
|
1029
1249
|
// claiming the same parent
|
|
@@ -1079,6 +1299,7 @@ export class Store {
|
|
|
1079
1299
|
t.messages.push(full);
|
|
1080
1300
|
t.activeLeafId = full.id;
|
|
1081
1301
|
mdb.appendMessage(threadId, full);
|
|
1302
|
+
this.noteThreadActivity(threadId, full.at);
|
|
1082
1303
|
this.emit({ type: "message", threadId, message: full });
|
|
1083
1304
|
// The message frame alone leaves every client on the OLD branch: a
|
|
1084
1305
|
// client adopts a new message as its leaf only when it chains onto the
|
|
@@ -1107,7 +1328,7 @@ export class Store {
|
|
|
1107
1328
|
this.emit({ type: "thread", threadId, activeLeafId: cur });
|
|
1108
1329
|
return cur;
|
|
1109
1330
|
}
|
|
1110
|
-
patchMessage(threadId, messageId, patch) {
|
|
1331
|
+
patchMessage(threadId, messageId, patch, command) {
|
|
1111
1332
|
const t = this.thread(threadId);
|
|
1112
1333
|
const idx = t.messages.findIndex((m) => m.id === messageId);
|
|
1113
1334
|
if (idx === -1)
|
|
@@ -1116,7 +1337,11 @@ export class Store {
|
|
|
1116
1337
|
// SQLite is the durable source of truth. Persist before changing memory so
|
|
1117
1338
|
// a failed write cannot make this process believe a card was answered
|
|
1118
1339
|
// while a restart would still show it as pending.
|
|
1119
|
-
|
|
1340
|
+
let applied = false;
|
|
1341
|
+
const persist = () => { mdb.updateMessage(threadId, next); applied = true; return next; };
|
|
1342
|
+
const committed = command ? runCommand(command, persist) : persist();
|
|
1343
|
+
if (!applied)
|
|
1344
|
+
return committed;
|
|
1120
1345
|
t.messages[idx] = next;
|
|
1121
1346
|
this.emit({ type: "message.patch", threadId, message: next });
|
|
1122
1347
|
return next;
|
|
@@ -1143,6 +1368,8 @@ export class Store {
|
|
|
1143
1368
|
color: profile.color ?? COLORS[this.bots.length % COLORS.length],
|
|
1144
1369
|
...(profile.mascotExpression ? { mascotExpression: profile.mascotExpression } : {}),
|
|
1145
1370
|
...(profile.mascotBody ? { mascotBody: profile.mascotBody } : {}),
|
|
1371
|
+
// Restricted from its first frame: no one else is ever told it exists.
|
|
1372
|
+
...(profile.visibility && profile.visibility !== "everyone" ? { visibility: structuredClone(profile.visibility) } : {}),
|
|
1146
1373
|
unread: false,
|
|
1147
1374
|
modelSelection: profile.modelSelection ?? this.defaultSelection(),
|
|
1148
1375
|
resumeCursors: {},
|
|
@@ -1154,6 +1381,7 @@ export class Store {
|
|
|
1154
1381
|
threadId: bot.threadId,
|
|
1155
1382
|
title: UNTITLED_THREAD,
|
|
1156
1383
|
createdAt: bot.createdAt,
|
|
1384
|
+
updatedAt: bot.createdAt,
|
|
1157
1385
|
resumeCursors: {},
|
|
1158
1386
|
modelSelection: structuredClone(bot.modelSelection),
|
|
1159
1387
|
unread: false,
|
|
@@ -1209,7 +1437,10 @@ export class Store {
|
|
|
1209
1437
|
title: "", description: "", soul: "", notifications: true, color: COLORS[nextBots.length % COLORS.length], unread: false,
|
|
1210
1438
|
modelSelection: operation.fields.modelSelection, resumeCursors: {}, createdAt, ...operation.fields,
|
|
1211
1439
|
approvalMode: "ask", autoApprove: false, composio: false, approvePeerComms: false,
|
|
1212
|
-
|
|
1440
|
+
// A Chief's new teammate is seen by exactly the Chief's audience:
|
|
1441
|
+
// a restricted Chief never creates a bot everyone sees.
|
|
1442
|
+
...(chief.visibility && chief.visibility !== "everyone" ? { visibility: structuredClone(chief.visibility) } : {}),
|
|
1443
|
+
tasks: [{ threadId: operation.threadId, title: UNTITLED_THREAD, createdAt, updatedAt: createdAt, resumeCursors: {},
|
|
1213
1444
|
modelSelection: structuredClone(operation.fields.modelSelection), approvalMode: "ask", autoApprove: false,
|
|
1214
1445
|
unread: false, activity: "idle", busy: false }],
|
|
1215
1446
|
};
|
|
@@ -1441,8 +1672,13 @@ export class Store {
|
|
|
1441
1672
|
const busy = ACTIVITY_BUSY.has(activity);
|
|
1442
1673
|
if ((task.activity ?? "idle") === activity && Boolean(task.busy) === busy)
|
|
1443
1674
|
return bot;
|
|
1675
|
+
const wasBusy = Boolean(task.busy);
|
|
1444
1676
|
task.activity = activity;
|
|
1445
1677
|
task.busy = busy;
|
|
1678
|
+
if (busy && !wasBusy)
|
|
1679
|
+
task.turnStartedAt = Date.now();
|
|
1680
|
+
else if (!busy)
|
|
1681
|
+
delete task.turnStartedAt;
|
|
1446
1682
|
this.refreshBotActivity(bot);
|
|
1447
1683
|
this.emit({ type: "bot", botId });
|
|
1448
1684
|
return bot;
|
|
@@ -1485,6 +1721,64 @@ export class Store {
|
|
|
1485
1721
|
this.emit({ type: "bot", botId: bot.id });
|
|
1486
1722
|
return changed;
|
|
1487
1723
|
}
|
|
1724
|
+
/** Company instance ids became stable across re-enrolment. Moves every
|
|
1725
|
+
* saved reference to an old id onto its replacement in one save: model
|
|
1726
|
+
* choices, native resume cursors and handed-message records. An entry that
|
|
1727
|
+
* already exists under the new id wins over the old one. */
|
|
1728
|
+
renameInstances(ids) {
|
|
1729
|
+
const touches = (record) => Boolean(record && Object.keys(record).some(key => ids.has(key)));
|
|
1730
|
+
const rename = (record) => {
|
|
1731
|
+
const next = {};
|
|
1732
|
+
for (const [key, value] of Object.entries(record)) {
|
|
1733
|
+
const target = ids.get(key);
|
|
1734
|
+
if (!target)
|
|
1735
|
+
next[key] = value;
|
|
1736
|
+
else if (!Object.prototype.hasOwnProperty.call(record, target))
|
|
1737
|
+
next[target] = value;
|
|
1738
|
+
}
|
|
1739
|
+
return next;
|
|
1740
|
+
};
|
|
1741
|
+
const changed = [];
|
|
1742
|
+
for (const bot of this.bots) {
|
|
1743
|
+
let dirty = false;
|
|
1744
|
+
const selected = ids.get(bot.modelSelection.instanceId);
|
|
1745
|
+
if (selected) {
|
|
1746
|
+
bot.modelSelection = { ...bot.modelSelection, instanceId: selected };
|
|
1747
|
+
dirty = true;
|
|
1748
|
+
}
|
|
1749
|
+
if (touches(bot.resumeCursors)) {
|
|
1750
|
+
bot.resumeCursors = rename(bot.resumeCursors);
|
|
1751
|
+
dirty = true;
|
|
1752
|
+
}
|
|
1753
|
+
for (const task of bot.tasks ?? []) {
|
|
1754
|
+
const taskSelected = task.modelSelection && ids.get(task.modelSelection.instanceId);
|
|
1755
|
+
if (task.modelSelection && taskSelected) {
|
|
1756
|
+
task.modelSelection = { ...task.modelSelection, instanceId: taskSelected };
|
|
1757
|
+
dirty = true;
|
|
1758
|
+
}
|
|
1759
|
+
if (touches(task.resumeCursors)) {
|
|
1760
|
+
task.resumeCursors = rename(task.resumeCursors);
|
|
1761
|
+
dirty = true;
|
|
1762
|
+
}
|
|
1763
|
+
if (task.handedMessages && touches(task.handedMessages)) {
|
|
1764
|
+
task.handedMessages = rename(task.handedMessages);
|
|
1765
|
+
dirty = true;
|
|
1766
|
+
}
|
|
1767
|
+
const last = task.lastInstanceId && ids.get(task.lastInstanceId);
|
|
1768
|
+
if (last) {
|
|
1769
|
+
task.lastInstanceId = last;
|
|
1770
|
+
dirty = true;
|
|
1771
|
+
}
|
|
1772
|
+
}
|
|
1773
|
+
if (dirty)
|
|
1774
|
+
changed.push(bot);
|
|
1775
|
+
}
|
|
1776
|
+
if (changed.length)
|
|
1777
|
+
this.saveBots();
|
|
1778
|
+
for (const bot of changed)
|
|
1779
|
+
this.emit({ type: "bot", botId: bot.id });
|
|
1780
|
+
return changed.length;
|
|
1781
|
+
}
|
|
1488
1782
|
setResumeCursor(botId, instanceId, cursor, threadId) {
|
|
1489
1783
|
const bot = this.bot(botId);
|
|
1490
1784
|
if (!bot)
|
|
@@ -1731,6 +2025,8 @@ export class Store {
|
|
|
1731
2025
|
}
|
|
1732
2026
|
if (typeof patch.title === "string")
|
|
1733
2027
|
task.title = patch.title.trim().slice(0, 80) || UNTITLED_THREAD;
|
|
2028
|
+
if (Object.prototype.hasOwnProperty.call(patch, "pinned") && task.pinned !== true)
|
|
2029
|
+
delete task.pinned;
|
|
1734
2030
|
if (bot.threadId === threadId)
|
|
1735
2031
|
this.mirrorActiveTask(bot, task);
|
|
1736
2032
|
bot.unread = bot.tasks.some((candidate) => candidate.unread);
|
|
@@ -1748,8 +2044,8 @@ export class Store {
|
|
|
1748
2044
|
return null;
|
|
1749
2045
|
const patch = { modelSelection: structuredClone(selection),
|
|
1750
2046
|
...(resetApprovalToAsk ? { approvalMode: "ask", autoApprove: false, alwaysAllow: [] } : {}) };
|
|
1751
|
-
const nextTask = { ...task, ...taskPatch, ...patch,
|
|
1752
|
-
...(typeof taskPatch.title === "string" ? { title: taskPatch.title.trim().slice(0, 80) || UNTITLED_THREAD } : {}) };
|
|
2047
|
+
const nextTask = persistedPin({ ...task, ...taskPatch, ...patch,
|
|
2048
|
+
...(typeof taskPatch.title === "string" ? { title: taskPatch.title.trim().slice(0, 80) || UNTITLED_THREAD } : {}) });
|
|
1753
2049
|
// Older threads may still inherit settings. Freeze their effective
|
|
1754
2050
|
// values before updating the default so "other threads unchanged" also
|
|
1755
2051
|
// holds for workspaces created before per-thread approval settings.
|
|
@@ -1769,6 +2065,20 @@ export class Store {
|
|
|
1769
2065
|
this.emit({ type: "bot", botId });
|
|
1770
2066
|
return task;
|
|
1771
2067
|
}
|
|
2068
|
+
/** One durable write: never leave only part of a bot's threads updated. */
|
|
2069
|
+
setAllThreadApprovalMode(botId, mode) {
|
|
2070
|
+
const bot = this.bot(botId);
|
|
2071
|
+
if (!bot)
|
|
2072
|
+
return null;
|
|
2073
|
+
const patch = { approvalMode: mode, autoApprove: false, alwaysAllow: [] };
|
|
2074
|
+
const tasks = (bot.tasks ?? []).map(task => ({ ...task, ...patch }));
|
|
2075
|
+
const next = { ...bot, ...patch, approvalGrant: undefined, tasks };
|
|
2076
|
+
this.saveBots(this.bots.map(candidate => candidate === bot ? next : candidate));
|
|
2077
|
+
bot.tasks?.forEach((task, index) => Object.assign(task, tasks[index]));
|
|
2078
|
+
Object.assign(bot, patch, { approvalGrant: undefined });
|
|
2079
|
+
this.emit({ type: "bot", botId });
|
|
2080
|
+
return bot;
|
|
2081
|
+
}
|
|
1772
2082
|
mirrorActiveTask(bot, task) {
|
|
1773
2083
|
bot.threadId = task.threadId;
|
|
1774
2084
|
bot.resumeCursors = structuredClone(task.resumeCursors);
|
|
@@ -1777,23 +2087,25 @@ export class Store {
|
|
|
1777
2087
|
}
|
|
1778
2088
|
/** A fresh context on the same bot: new thread, new session, same
|
|
1779
2089
|
* persona/tools/computer. Becomes the active task. */
|
|
1780
|
-
createTask(botId, title, activate = true, projectId, openedBy) {
|
|
2090
|
+
createTask(botId, title, activate = true, projectId, openedBy, approvalMode) {
|
|
1781
2091
|
const bot = this.bot(botId);
|
|
1782
2092
|
if (!bot)
|
|
1783
2093
|
return null;
|
|
1784
2094
|
if (projectId !== undefined && !this.project(botId, projectId))
|
|
1785
2095
|
return null;
|
|
2096
|
+
const createdAt = Date.now();
|
|
1786
2097
|
const task = {
|
|
1787
2098
|
threadId: newId(),
|
|
1788
2099
|
title: threadTitleFrom(title),
|
|
1789
|
-
createdAt
|
|
2100
|
+
createdAt,
|
|
2101
|
+
updatedAt: createdAt,
|
|
1790
2102
|
...(projectId ? { projectId } : {}),
|
|
1791
2103
|
...(openedBy ? { openedBy: structuredClone(openedBy) } : {}),
|
|
1792
2104
|
resumeCursors: {},
|
|
1793
2105
|
modelSelection: structuredClone(bot.modelSelection),
|
|
1794
|
-
approvalMode: approvalModeFor(bot),
|
|
1795
|
-
autoApprove: Boolean(bot.autoApprove),
|
|
1796
|
-
alwaysAllow: [...(bot.alwaysAllow ?? [])],
|
|
2106
|
+
approvalMode: approvalMode ?? approvalModeFor(bot),
|
|
2107
|
+
autoApprove: approvalMode ? false : Boolean(bot.autoApprove),
|
|
2108
|
+
alwaysAllow: approvalMode ? [] : [...(bot.alwaysAllow ?? [])],
|
|
1797
2109
|
unread: false,
|
|
1798
2110
|
activity: "idle",
|
|
1799
2111
|
busy: false,
|
|
@@ -1866,12 +2178,29 @@ export class Store {
|
|
|
1866
2178
|
* interleave in one transcript. `label` names that thread; the caller
|
|
1867
2179
|
* closes it once its result has been reported. A pair conversation
|
|
1868
2180
|
* never auto-closes. */
|
|
2181
|
+
/** The identity a peer-opened row belongs to: its opener's id while that
|
|
2182
|
+
* bot lives, else the one live bot the stamp's name still points at —
|
|
2183
|
+
* what a deleted-and-recreated same-name bot inherits — else the dead id
|
|
2184
|
+
* itself. A name two live bots share resolves to nobody's twin:
|
|
2185
|
+
* ambiguous means unmatched, never a wrong merge. */
|
|
2186
|
+
openerIdentity(openedBy) {
|
|
2187
|
+
if (this.bot(openedBy.botId))
|
|
2188
|
+
return openedBy.botId;
|
|
2189
|
+
const named = this.bots.filter((bot) => bot.name === openedBy.name);
|
|
2190
|
+
return named.length === 1 ? named[0].id : openedBy.botId;
|
|
2191
|
+
}
|
|
1869
2192
|
resolvePairConversation(sender, recipientId, options) {
|
|
1870
2193
|
if (!this.bot(recipientId))
|
|
1871
2194
|
return null;
|
|
1872
2195
|
const title = `@${sender.name}`;
|
|
1873
2196
|
const opener = (kind, at = Date.now()) => ({ botId: sender.id, name: sender.name, kind, at });
|
|
1874
|
-
|
|
2197
|
+
// Identity-stable: the sender's own rows, plus ones a deleted
|
|
2198
|
+
// predecessor opened when the stamp's name still points at exactly
|
|
2199
|
+
// this bot — a same-name recreation inherits the conversation instead
|
|
2200
|
+
// of minting a twin while the old row dangles live. A name two live
|
|
2201
|
+
// bots share matches nobody's inheritance: refusing the fallback can
|
|
2202
|
+
// cost a new row, never merge two bots' histories.
|
|
2203
|
+
const fromSender = this.tasks(recipientId).filter((task) => task.openedBy && this.openerIdentity(task.openedBy) === sender.id);
|
|
1875
2204
|
let pair = fromSender.find((task) => task.openedBy?.kind === "pair");
|
|
1876
2205
|
if (!pair) {
|
|
1877
2206
|
const lastActivity = (task) => this.messagesTail(task.threadId, 1).messages.at(-1)?.at ?? task.openedBy?.at ?? task.createdAt;
|
|
@@ -1900,6 +2229,14 @@ export class Store {
|
|
|
1900
2229
|
// back up, never replaced: closing is only the sidebar's idle state.
|
|
1901
2230
|
if (pair.closedBy)
|
|
1902
2231
|
this.setTaskClosedBy(recipientId, pair.threadId, null);
|
|
2232
|
+
// An inherited row carries the predecessor's id; rebind it to the
|
|
2233
|
+
// live bot so the name fallback is needed only once — a namesake
|
|
2234
|
+
// appearing later cannot claim the row. The hour it was opened
|
|
2235
|
+
// stays: inheritance is not a new conversation.
|
|
2236
|
+
const inherited = pair.openedBy;
|
|
2237
|
+
if (inherited && inherited.botId !== sender.id) {
|
|
2238
|
+
this.setTaskOpenedBy(recipientId, pair.threadId, { ...inherited, botId: sender.id, name: sender.name });
|
|
2239
|
+
}
|
|
1903
2240
|
return { task: pair, created: false };
|
|
1904
2241
|
}
|
|
1905
2242
|
// The brief is never a title. An 80-character slice of an assignment
|
|
@@ -1932,14 +2269,28 @@ export class Store {
|
|
|
1932
2269
|
renameTask(botId, threadId, title) {
|
|
1933
2270
|
return this.patchTask(botId, threadId, { title });
|
|
1934
2271
|
}
|
|
1935
|
-
/** Name a task after its first message, once.
|
|
2272
|
+
/** Name a task after its first message, once. Returns the task it named
|
|
2273
|
+
* so a caller can later replace exactly that machine-made title — and
|
|
2274
|
+
* can see the peer provenance it must leave alone. */
|
|
1936
2275
|
titleTaskFromFirstMessage(botId, text, threadId) {
|
|
1937
2276
|
const task = threadId ? this.taskByThread(botId, threadId) : this.activeTask(botId);
|
|
1938
|
-
if (!task || (task.title !== UNTITLED_TASK && task.title !== UNTITLED_THREAD))
|
|
1939
|
-
return;
|
|
2277
|
+
if (!task || task.titleFromFirstMessage || (task.title !== UNTITLED_TASK && task.title !== UNTITLED_THREAD))
|
|
2278
|
+
return null;
|
|
1940
2279
|
task.title = titleFromMessage(text);
|
|
2280
|
+
task.titleFromFirstMessage = true;
|
|
1941
2281
|
this.saveBots();
|
|
1942
2282
|
this.emit({ type: "bot", botId });
|
|
2283
|
+
return task;
|
|
2284
|
+
}
|
|
2285
|
+
/** Swap a machine-made first-message title for a generated one, once.
|
|
2286
|
+
* Equality against the snippet is the whole contract: a rename by the
|
|
2287
|
+
* person, by pair adoption, or by an earlier generated title each break
|
|
2288
|
+
* it, so this never overwrites a name anyone chose. */
|
|
2289
|
+
retitleTask(botId, threadId, machineTitle, title) {
|
|
2290
|
+
const task = this.taskByThread(botId, threadId);
|
|
2291
|
+
if (!task || task.title !== machineTitle)
|
|
2292
|
+
return null;
|
|
2293
|
+
return this.renameTask(botId, threadId, threadTitleFrom(title));
|
|
1943
2294
|
}
|
|
1944
2295
|
/** Delete a task and its transcript, retaining generated project files.
|
|
1945
2296
|
* When no visible tasks remain, replace it with a fresh conversation. */
|