openmausbot 0.1.87 → 0.1.89
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 -1
- package/dist/assets/{abnfDiagram-O67JEVCF-BTiuL79F.js → abnfDiagram-O67JEVCF-BwaMaZEm.js} +1 -1
- package/dist/assets/{arc-C88Blc2v.js → arc-Be-8GKxd.js} +1 -1
- package/dist/assets/{architectureDiagram-NJMV4G6O-BZsJIZrb.js → architectureDiagram-NJMV4G6O-Dp_U2Kv1.js} +1 -1
- package/dist/assets/{blockDiagram-BEXU5L5S-CVeMe9Ah.js → blockDiagram-BEXU5L5S-DQjUjYUk.js} +1 -1
- package/dist/assets/{c4Diagram-YGBWAQC7-Cedk3Bhx.js → c4Diagram-YGBWAQC7-Bd3aE4Kc.js} +1 -1
- package/dist/assets/channel-DqY_gMLg.js +1 -0
- package/dist/assets/{chunk-2Q5K7J3B-D-VRqzTB.js → chunk-2Q5K7J3B-BijjTBeJ.js} +1 -1
- package/dist/assets/{chunk-3FUC2YCW-BChov01Y.js → chunk-3FUC2YCW-CMGdwvzW.js} +2 -2
- package/dist/assets/{chunk-5VM5RSS4-C-TfwvHy.js → chunk-5VM5RSS4-0G09BfsZ.js} +1 -1
- package/dist/assets/{chunk-GTNCS2PH-Db7CGj5Y.js → chunk-GTNCS2PH-FOp3vSHe.js} +1 -1
- package/dist/assets/{chunk-GWA4HPMP-CNvIfqKT.js → chunk-GWA4HPMP-Dv-20VIn.js} +1 -1
- package/dist/assets/{chunk-JWPE2WC7-C_bEp5tq.js → chunk-JWPE2WC7-CRgrdFcz.js} +1 -1
- package/dist/assets/{chunk-NETBCI7D-Coi4BOls.js → chunk-NETBCI7D-3hP4f7QB.js} +1 -1
- package/dist/assets/{chunk-WEXAMYUT-CzylEuL3.js → chunk-WEXAMYUT-CG98IjlG.js} +1 -1
- package/dist/assets/{chunk-XXDRQBXY-m7p7ztaD.js → chunk-XXDRQBXY-BrSTC9xG.js} +1 -1
- package/dist/assets/{classDiagram-v2-NBCMYWYE-BMy6o9x2.js → classDiagram-v2-NBCMYWYE-rpQQQDFX.js} +1 -1
- package/dist/assets/{cose-bilkent-JH36ORCC-BDQ__0_J.js → cose-bilkent-JH36ORCC-G6RSafHA.js} +1 -1
- package/dist/assets/{cynefin-OW5HDTMX-Djv4ghUb.js → cynefin-OW5HDTMX-CXjv-IM2.js} +1 -1
- package/dist/assets/{cynefinDiagram-VND7K2PF-BULrlGYH.js → cynefinDiagram-VND7K2PF-Dw07MR9f.js} +1 -1
- package/dist/assets/{dagre-6A5THRUB-D-jrT_hU.js → dagre-6A5THRUB-BwPnlTg2.js} +1 -1
- package/dist/assets/{diagram-22UHCM2B-p2UhQmfL.js → diagram-22UHCM2B-Cm6xcMiz.js} +1 -1
- package/dist/assets/{diagram-3UASUU5V-DHvGWZ7q.js → diagram-3UASUU5V-CDpogPbE.js} +1 -1
- package/dist/assets/{diagram-ATOU4E4O-ChhKDot-.js → diagram-ATOU4E4O-BV8z3Pnc.js} +1 -1
- package/dist/assets/{diagram-CDSNMT55-B-T7A43R.js → diagram-CDSNMT55-CKCQhOOC.js} +1 -1
- package/dist/assets/{diagram-MLGK6HIB-CJm8ENNY.js → diagram-MLGK6HIB-FGvJg69C.js} +1 -1
- package/dist/assets/{diagram-MPIPVDR6-510tlza8.js → diagram-MPIPVDR6-CwATvgnS.js} +1 -1
- package/dist/assets/{ebnfDiagram-ZINNZB2B-PkvRPHel.js → ebnfDiagram-ZINNZB2B-CH_c3SSC.js} +1 -1
- package/dist/assets/{elk-276RUBZZ-BVBVPoX3.js → elk-276RUBZZ-3guZi71R.js} +1 -1
- package/dist/assets/{erDiagram-OPXOYQCR-BgYMVm7L.js → erDiagram-OPXOYQCR-BMHphR2n.js} +1 -1
- package/dist/assets/{flowDiagram-KWPJA3E3-BGqsnc_3.js → flowDiagram-KWPJA3E3-Vm5DkW5x.js} +1 -1
- package/dist/assets/{ganttDiagram-FUAMR5RP-D_-UeReH.js → ganttDiagram-FUAMR5RP-DO-1ct07.js} +1 -1
- package/dist/assets/{gitGraphDiagram-X574FWY7-D0uooONx.js → gitGraphDiagram-X574FWY7-nxeksEIN.js} +1 -1
- package/dist/assets/index-BIMdIbaw.css +1 -0
- package/dist/assets/index-BVPdbq2r.js +583 -0
- package/dist/assets/{index-zRsJERGJ.js → index-Oxk9x45i.js} +1 -1
- package/dist/assets/{infoDiagram-VRGFBTTK-B9e1z77f.js → infoDiagram-VRGFBTTK-Dkcvr0p7.js} +1 -1
- package/dist/assets/{ishikawaDiagram-OU5B5YK6-BB4vNwvD.js → ishikawaDiagram-OU5B5YK6-BwVibkoO.js} +1 -1
- package/dist/assets/{journeyDiagram-ZHPQQLJL-DpkosOTB.js → journeyDiagram-ZHPQQLJL-ByiBZqgG.js} +1 -1
- package/dist/assets/{kanban-definition-PNTS6WVX-DbDP4kUu.js → kanban-definition-PNTS6WVX-qqjgXBF3.js} +1 -1
- package/dist/assets/{linear-DBJGPmjT.js → linear-CtJnKe6A.js} +1 -1
- package/dist/assets/{mermaid.core-DWPRi-ue.js → mermaid.core-BEiDklrY.js} +5 -5
- package/dist/assets/{mindmap-definition-NLK3R4M7-nszBbJmx.js → mindmap-definition-NLK3R4M7-DfIYHia9.js} +1 -1
- package/dist/assets/{pegDiagram-GJSIUBJH-CWykJWu_.js → pegDiagram-GJSIUBJH-CYyRqa1a.js} +1 -1
- package/dist/assets/{pieDiagram-5QR66LMP-g3Da-Vnv.js → pieDiagram-5QR66LMP-hOToy_zH.js} +1 -1
- package/dist/assets/{quadrantDiagram-O4NWA36T-Wzrlpzgh.js → quadrantDiagram-O4NWA36T-DKAsNN9I.js} +1 -1
- package/dist/assets/{railroadDiagram-XR7U4H2S-moHTFMC0.js → railroadDiagram-XR7U4H2S-BSA-d8JU.js} +1 -1
- package/dist/assets/{requirementDiagram-PLB6GJNP-CwfXdFH8.js → requirementDiagram-PLB6GJNP-CSmlvb-p.js} +1 -1
- package/dist/assets/{sankeyDiagram-IPEJSGJF-3ZROYvnq.js → sankeyDiagram-IPEJSGJF-CD5F6fnG.js} +1 -1
- package/dist/assets/{sequenceDiagram-PO4LG4MO-E1TBebP8.js → sequenceDiagram-PO4LG4MO-JUq5sx6d.js} +1 -1
- package/dist/assets/{sizeCapture-INFHLROL-DHKEU5pu.js → sizeCapture-INFHLROL-BjIS-Stk.js} +1 -1
- package/dist/assets/{stateDiagram-v2-GCMORJYK-BguEj09D.js → stateDiagram-v2-GCMORJYK-DklkUxOg.js} +1 -1
- package/dist/assets/{swimlanes-2SLR337P-C7IDYnhd.js → swimlanes-2SLR337P-CsjIU6be.js} +1 -1
- package/dist/assets/swimlanesDiagram-TC7HE7FX-Bk14M0_B.js +8 -0
- package/dist/assets/{timeline-definition-EJHVYXUP-DCW-F5PW.js → timeline-definition-EJHVYXUP-BkFqK7AB.js} +1 -1
- package/dist/assets/{usecaseDiagram-POWQR4AR-xBMFtWYV.js → usecaseDiagram-POWQR4AR-70gJB1EU.js} +1 -1
- package/dist/assets/{vennDiagram-UO4OBE2U-B5nIkuH8.js → vennDiagram-UO4OBE2U-83Tm62dN.js} +1 -1
- package/dist/assets/{wardleyDiagram-VNRHLVJA-DYFAhdbr.js → wardleyDiagram-VNRHLVJA-tTmseoX9.js} +1 -1
- package/dist/assets/{xychartDiagram-PMCCYNJV-DeXwX6Cz.js → xychartDiagram-PMCCYNJV-R1LBA55a.js} +1 -1
- package/dist/index.html +2 -2
- package/dist-server/companion/src/routes.js +8 -1
- package/dist-server/connector-proxy.js +105 -1
- package/dist-server/container-mcp.js +11 -1
- package/dist-server/drivers/agents-proxy.js +277 -22
- package/dist-server/index.js +23722 -20272
- package/dist-server/local-computer-proxy.js +2 -0
- package/dist-server/mcp-gate.js +2 -0
- package/dist-server/mcp-server.js +5 -3
- package/dist-server/message-search.worker.js +52 -0
- package/dist-server/openmausbot.js +755 -404
- package/dist-server/pair-cli.js +755 -404
- package/dist-server/server/admission.js +81 -0
- package/dist-server/server/aside-queue.js +294 -0
- package/dist-server/server/attachments.js +43 -1
- package/dist-server/server/automatic-recovery.js +15 -0
- package/dist-server/server/bot-attachment.js +107 -0
- package/dist-server/server/bot-overview.js +19 -0
- package/dist-server/server/composio.js +297 -1
- package/dist-server/server/config.js +34 -3
- package/dist-server/server/connector-advertisement.js +165 -0
- package/dist-server/server/connector-proxy.js +14 -2
- package/dist-server/server/connector-verdict.js +31 -4
- package/dist-server/server/container-computer.js +70 -0
- package/dist-server/server/contracts.js +32 -0
- package/dist-server/server/drivers/acp/core.js +148 -23
- package/dist-server/server/drivers/acp/cursor.js +3 -7
- package/dist-server/server/drivers/acp/droid.js +4 -7
- package/dist-server/server/drivers/acp/gemini.js +4 -4
- package/dist-server/server/drivers/acp/grok.js +4 -9
- package/dist-server/server/drivers/acp/hermes.js +3 -5
- package/dist-server/server/drivers/acp/opencode-go.js +2 -5
- package/dist-server/server/drivers/acp/qwen.js +12 -15
- package/dist-server/server/drivers/agents-call.js +137 -10
- package/dist-server/server/drivers/agents-catalog.js +100 -6
- package/dist-server/server/drivers/boxagent.js +3 -17
- package/dist-server/server/drivers/claude.js +31 -17
- package/dist-server/server/drivers/codex-catalog.js +14 -6
- package/dist-server/server/drivers/codex-identity.js +2 -1
- package/dist-server/server/drivers/codex-release.js +48 -0
- package/dist-server/server/drivers/codex.js +7 -25
- package/dist-server/server/drivers/openai-chat.js +31 -3
- package/dist-server/server/drivers/pi.js +237 -30
- package/dist-server/server/drivers/prompt-split.js +32 -9
- package/dist-server/server/drivers/session-idle.js +29 -0
- package/dist-server/server/env-path.js +15 -0
- package/dist-server/server/guarded-requests.js +2 -2
- package/dist-server/server/harness-capabilities.js +61 -0
- package/dist-server/server/index.js +1173 -262
- package/dist-server/server/message-db.js +22 -42
- package/dist-server/server/message-file.js +9 -1
- package/dist-server/server/message-search-query.js +43 -0
- package/dist-server/server/message-search-worker.js +80 -0
- package/dist-server/server/message-search.worker.js +21 -0
- package/dist-server/server/model-requests.js +248 -0
- package/dist-server/server/options-card.js +21 -0
- package/dist-server/server/org-library.js +15 -1
- package/dist-server/server/peer-delivery.js +60 -0
- package/dist-server/server/peer-roster.js +57 -0
- package/dist-server/server/presets.js +32 -32
- package/dist-server/server/profile-requests.js +66 -39
- package/dist-server/server/profile-revision.js +9 -1
- package/dist-server/server/profile-versions.js +6 -3
- package/dist-server/server/routine-requests.js +29 -7
- package/dist-server/server/skill-fetch.js +72 -14
- package/dist-server/server/skills.js +1 -0
- package/dist-server/server/store.js +32 -2
- package/dist-server/server/system-prompt.js +37 -2
- package/dist-server/server/team-setup-requests.js +124 -23
- package/dist-server/server/thread-events.js +3 -1
- package/dist-server/server/tightening-requests.js +405 -0
- package/dist-server/server/tts/chatterbox.js +27 -5
- package/dist-server/server/usage-ledger.js +7 -1
- package/dist-server/server/webhooks.js +21 -3
- package/dist-server/server/workspace-backup.js +40 -3
- package/dist-server/server/workspace-backup.worker.js +10 -0
- package/dist-server/shared/ask-question.js +19 -0
- package/dist-server/shared/credential-request.js +5 -1
- package/dist-server/shared/line-diff.js +23 -0
- package/dist-server/shared/model-request.js +1 -0
- package/dist-server/shared/options-card.js +51 -0
- package/dist-server/shared/profile-request.js +5 -4
- package/dist-server/shared/tightening-request.js +87 -0
- package/dist-server/src/lib/composer-attachments.js +42 -3
- package/dist-server/vps-container-mcp.js +11 -1
- package/dist-server/workspace-backup.worker.js +19482 -0
- package/package.json +1 -1
- package/dist/assets/channel-taCNRlUU.js +0 -1
- package/dist/assets/index-BwdMAQ6p.css +0 -1
- package/dist/assets/index-ByJennr7.js +0 -581
- package/dist/assets/swimlanesDiagram-TC7HE7FX-BpEdoB2e.js +0 -8
|
@@ -15,9 +15,13 @@ import { join } from "node:path";
|
|
|
15
15
|
import { DatabaseSync } from "node:sqlite";
|
|
16
16
|
import { DATA_DIR } from "./config.js";
|
|
17
17
|
import { peerProvenanceAuthor } from "./peer-provenance.js";
|
|
18
|
+
import { MessageSearchWorker } from "./message-search-worker.js";
|
|
19
|
+
import { searchMessagesInDatabase } from "./message-search-query.js";
|
|
18
20
|
const DB_FILE = () => join(DATA_DIR, "messages.db");
|
|
19
21
|
let handle = null;
|
|
20
22
|
let handlePath = null;
|
|
23
|
+
let searchWorker = null;
|
|
24
|
+
let closingSearch = Promise.resolve();
|
|
21
25
|
function open() {
|
|
22
26
|
mkdirSync(DATA_DIR, { recursive: true, mode: 0o700 });
|
|
23
27
|
const file = DB_FILE();
|
|
@@ -444,49 +448,16 @@ export function threadsReferencing(fragment) {
|
|
|
444
448
|
return rows.map((row) => row.thread_id);
|
|
445
449
|
}
|
|
446
450
|
/** Case-insensitive substring search over text messages, newest first.
|
|
447
|
-
*
|
|
448
|
-
* scan is milliseconds, and it needs no FTS extension to exist. */
|
|
451
|
+
* Keep literal substring semantics; HTTP callers run the scan off-thread. */
|
|
449
452
|
export function searchMessages(query, limit = 40, threadId) {
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
//
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
const scope = threadId ? "thread_id = ? AND " : "";
|
|
459
|
-
const statement = db().prepare("SELECT thread_id, id, at, role, kind, text, json_extract(json, '$.tool.name') AS tool_name, json_extract(json, '$.from.name') AS from_name FROM messages " +
|
|
460
|
-
`WHERE ${scope}((kind = 'text' AND text IS NOT NULL AND lower(text) LIKE ? ESCAPE '\\') ` +
|
|
461
|
-
" OR (kind = 'activity' AND tool_name IS NOT NULL AND lower(tool_name) LIKE ? ESCAPE '\\')) " +
|
|
462
|
-
"ORDER BY at DESC LIMIT ?");
|
|
463
|
-
const rows = (threadId
|
|
464
|
-
? statement.all(threadId, pattern, pattern, limit)
|
|
465
|
-
: statement.all(pattern, pattern, limit));
|
|
466
|
-
return rows.map((row) => {
|
|
467
|
-
const haystack = row.kind === "activity" ? (row.tool_name ?? "") : (row.text ?? "");
|
|
468
|
-
const hitAt = Math.max(0, haystack.toLowerCase().indexOf(needle));
|
|
469
|
-
const start = Math.max(0, hitAt - 60);
|
|
470
|
-
const end = Math.min(haystack.length, hitAt + needle.length + 90);
|
|
471
|
-
const head = start > 0 ? "…" : "";
|
|
472
|
-
const body = haystack.slice(start, end).replace(/\s+/g, " ").trim();
|
|
473
|
-
const snippet = head + body + (end < haystack.length ? "…" : "");
|
|
474
|
-
// whitespace folding can shift the offset; find the match again inside
|
|
475
|
-
const folded = needle.replace(/\s+/g, " ");
|
|
476
|
-
const matchStart = snippet.toLowerCase().indexOf(folded);
|
|
477
|
-
return {
|
|
478
|
-
threadId: row.thread_id,
|
|
479
|
-
messageId: row.id,
|
|
480
|
-
at: row.at,
|
|
481
|
-
role: row.role,
|
|
482
|
-
kind: row.kind,
|
|
483
|
-
snippet,
|
|
484
|
-
matchStart: matchStart < 0 ? head.length : matchStart,
|
|
485
|
-
// A defensive fallback must not mark arbitrary snippet text as the hit.
|
|
486
|
-
matchLength: matchStart < 0 ? 0 : folded.length,
|
|
487
|
-
...(row.from_name ? { from: row.from_name } : {}),
|
|
488
|
-
};
|
|
489
|
-
});
|
|
453
|
+
return searchMessagesInDatabase(db(), query, limit, threadId);
|
|
454
|
+
}
|
|
455
|
+
export function searchMessagesAsync(query, limit = 40, threadId) {
|
|
456
|
+
if (!query.trim())
|
|
457
|
+
return Promise.resolve([]);
|
|
458
|
+
db(); // The owner initializes/migrates the schema before any read-only scan.
|
|
459
|
+
searchWorker ??= new MessageSearchWorker(DB_FILE());
|
|
460
|
+
return searchWorker.search(query, limit, threadId);
|
|
490
461
|
}
|
|
491
462
|
/** Who wrote a user-role line that reads as the user's. The structural
|
|
492
463
|
* field wins; rows stored before it existed still open with the note. */
|
|
@@ -675,8 +646,17 @@ export function recallMemory(query, botId, limit = 12) {
|
|
|
675
646
|
.all(match, botId, limit);
|
|
676
647
|
return rows.map((row) => ({ file: row.path, at: row.mtime_ms, snippet: row.snippet.replace(/\s+/g, " ").trim() }));
|
|
677
648
|
}
|
|
649
|
+
/** Await before maintenance replaces the database or shutdown releases its lease. */
|
|
650
|
+
export async function closeMessageSearch() {
|
|
651
|
+
const worker = searchWorker;
|
|
652
|
+
searchWorker = null;
|
|
653
|
+
if (worker)
|
|
654
|
+
closingSearch = Promise.all([closingSearch, worker.close()]).then(() => { });
|
|
655
|
+
await closingSearch;
|
|
656
|
+
}
|
|
678
657
|
/** Test/shutdown hook — closes the handle so a wiped DATA_DIR starts clean. */
|
|
679
658
|
export function closeMessageDb() {
|
|
659
|
+
void closeMessageSearch();
|
|
680
660
|
try {
|
|
681
661
|
handle?.close();
|
|
682
662
|
}
|
|
@@ -313,7 +313,7 @@ function containedBy(root, candidate) {
|
|
|
313
313
|
const suffix = relative(root, candidate);
|
|
314
314
|
return suffix === "" || (suffix !== ".." && !suffix.startsWith(`..${sep}`) && !isAbsolute(suffix));
|
|
315
315
|
}
|
|
316
|
-
function mimeFor(path) {
|
|
316
|
+
export function mimeFor(path) {
|
|
317
317
|
switch (extname(path).toLowerCase()) {
|
|
318
318
|
case ".md": return "text/markdown; charset=utf-8";
|
|
319
319
|
case ".txt": return "text/plain; charset=utf-8";
|
|
@@ -331,6 +331,14 @@ function mimeFor(path) {
|
|
|
331
331
|
case ".m4v": return "video/x-m4v";
|
|
332
332
|
case ".webm": return "video/webm";
|
|
333
333
|
case ".mov": return "video/quicktime";
|
|
334
|
+
case ".mp3": return "audio/mpeg";
|
|
335
|
+
case ".m4a": return "audio/mp4";
|
|
336
|
+
case ".aac": return "audio/aac";
|
|
337
|
+
case ".wav": return "audio/wav";
|
|
338
|
+
case ".ogg":
|
|
339
|
+
case ".oga": return "audio/ogg";
|
|
340
|
+
case ".opus": return "audio/opus";
|
|
341
|
+
case ".flac": return "audio/flac";
|
|
334
342
|
case ".doc": return "application/msword";
|
|
335
343
|
case ".docx": return "application/vnd.openxmlformats-officedocument.wordprocessingml.document";
|
|
336
344
|
case ".xls": return "application/vnd.ms-excel";
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/** Shared query and snippet logic; the worker never calls the write-side open(). */
|
|
2
|
+
export function searchMessagesInDatabase(database, query, limit = 40, threadId) {
|
|
3
|
+
const needle = query.trim().toLowerCase();
|
|
4
|
+
if (!needle)
|
|
5
|
+
return [];
|
|
6
|
+
// escape LIKE wildcards so a literal % or _ in the query stays literal
|
|
7
|
+
const pattern = `%${needle.replace(/([\\%_])/g, "\\$1")}%`;
|
|
8
|
+
// text messages by their text; activity chips by the tool name — "which
|
|
9
|
+
// bot ran that migration" is a tool-name question. The chip's name lives
|
|
10
|
+
// in the row's json; a JSON1 extract keeps this one query.
|
|
11
|
+
const scope = threadId ? "thread_id = ? AND " : "";
|
|
12
|
+
const statement = database.prepare("SELECT thread_id, id, at, role, kind, text, json_extract(json, '$.tool.name') AS tool_name, json_extract(json, '$.from.name') AS from_name FROM messages " +
|
|
13
|
+
`WHERE ${scope}((kind = 'text' AND text IS NOT NULL AND lower(text) LIKE ? ESCAPE '\\') ` +
|
|
14
|
+
" OR (kind = 'activity' AND tool_name IS NOT NULL AND lower(tool_name) LIKE ? ESCAPE '\\')) " +
|
|
15
|
+
"ORDER BY at DESC LIMIT ?");
|
|
16
|
+
const rows = (threadId
|
|
17
|
+
? statement.all(threadId, pattern, pattern, limit)
|
|
18
|
+
: statement.all(pattern, pattern, limit));
|
|
19
|
+
return rows.map((row) => {
|
|
20
|
+
const haystack = row.kind === "activity" ? (row.tool_name ?? "") : (row.text ?? "");
|
|
21
|
+
const hitAt = Math.max(0, haystack.toLowerCase().indexOf(needle));
|
|
22
|
+
const start = Math.max(0, hitAt - 60);
|
|
23
|
+
const end = Math.min(haystack.length, hitAt + needle.length + 90);
|
|
24
|
+
const head = start > 0 ? "…" : "";
|
|
25
|
+
const body = haystack.slice(start, end).replace(/\s+/g, " ").trim();
|
|
26
|
+
const snippet = head + body + (end < haystack.length ? "…" : "");
|
|
27
|
+
// whitespace folding can shift the offset; find the match again inside
|
|
28
|
+
const folded = needle.replace(/\s+/g, " ");
|
|
29
|
+
const matchStart = snippet.toLowerCase().indexOf(folded);
|
|
30
|
+
return {
|
|
31
|
+
threadId: row.thread_id,
|
|
32
|
+
messageId: row.id,
|
|
33
|
+
at: row.at,
|
|
34
|
+
role: row.role,
|
|
35
|
+
kind: row.kind,
|
|
36
|
+
snippet,
|
|
37
|
+
matchStart: matchStart < 0 ? head.length : matchStart,
|
|
38
|
+
// A defensive fallback must not mark arbitrary snippet text as the hit.
|
|
39
|
+
matchLength: matchStart < 0 ? 0 : folded.length,
|
|
40
|
+
...(row.from_name ? { from: row.from_name } : {}),
|
|
41
|
+
};
|
|
42
|
+
});
|
|
43
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { existsSync } from "node:fs";
|
|
2
|
+
import { Worker } from "node:worker_threads";
|
|
3
|
+
export class SearchUnavailable extends Error {
|
|
4
|
+
status = 503;
|
|
5
|
+
}
|
|
6
|
+
/** One read-only worker, not a worker per keystroke. Bound outstanding scans
|
|
7
|
+
* so a burst of searches cannot build an unbounded queue behind the database. */
|
|
8
|
+
export class MessageSearchWorker {
|
|
9
|
+
worker;
|
|
10
|
+
stopping = Promise.resolve();
|
|
11
|
+
sequence = 0;
|
|
12
|
+
pending = new Map();
|
|
13
|
+
file;
|
|
14
|
+
timeoutMs;
|
|
15
|
+
constructor(file, timeoutMs = 30_000) {
|
|
16
|
+
this.file = file;
|
|
17
|
+
this.timeoutMs = timeoutMs;
|
|
18
|
+
}
|
|
19
|
+
search(query, limit, threadId) {
|
|
20
|
+
if (this.pending.size >= 8)
|
|
21
|
+
return Promise.reject(new SearchUnavailable("Search is busy. Try again shortly."));
|
|
22
|
+
if (!this.worker) {
|
|
23
|
+
const source = new URL("./message-search.worker.ts", import.meta.url);
|
|
24
|
+
const worker = new Worker(existsSync(source) ? source : new URL("./message-search.worker.js", import.meta.url), {
|
|
25
|
+
workerData: { file: this.file },
|
|
26
|
+
// Do not inherit test loaders or inspector ports into the worker.
|
|
27
|
+
execArgv: [],
|
|
28
|
+
});
|
|
29
|
+
this.worker = worker;
|
|
30
|
+
worker.on("message", (reply) => {
|
|
31
|
+
if (this.worker !== worker)
|
|
32
|
+
return;
|
|
33
|
+
const request = this.pending.get(reply.id);
|
|
34
|
+
if (!request)
|
|
35
|
+
return;
|
|
36
|
+
clearTimeout(request.timer);
|
|
37
|
+
this.pending.delete(reply.id);
|
|
38
|
+
if (reply.failed)
|
|
39
|
+
request.reject(new SearchUnavailable("Search could not finish. Try again shortly."));
|
|
40
|
+
else
|
|
41
|
+
request.resolve(reply.hits ?? []);
|
|
42
|
+
if (!this.pending.size)
|
|
43
|
+
worker.unref();
|
|
44
|
+
});
|
|
45
|
+
worker.on("error", () => {
|
|
46
|
+
if (this.worker === worker)
|
|
47
|
+
void this.close();
|
|
48
|
+
});
|
|
49
|
+
worker.on("exit", () => {
|
|
50
|
+
if (this.worker === worker)
|
|
51
|
+
void this.close();
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
const worker = this.worker;
|
|
55
|
+
worker.ref();
|
|
56
|
+
const id = ++this.sequence;
|
|
57
|
+
return new Promise((resolve, reject) => {
|
|
58
|
+
const timer = setTimeout(() => { void this.close(); }, this.timeoutMs);
|
|
59
|
+
this.pending.set(id, { resolve, reject, timer });
|
|
60
|
+
try {
|
|
61
|
+
worker.postMessage({ id, query, limit, threadId });
|
|
62
|
+
}
|
|
63
|
+
catch {
|
|
64
|
+
void this.close();
|
|
65
|
+
}
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
async close() {
|
|
69
|
+
const worker = this.worker;
|
|
70
|
+
this.worker = undefined;
|
|
71
|
+
for (const request of this.pending.values()) {
|
|
72
|
+
clearTimeout(request.timer);
|
|
73
|
+
request.reject(new SearchUnavailable("Search was interrupted. Try again shortly."));
|
|
74
|
+
}
|
|
75
|
+
this.pending.clear();
|
|
76
|
+
if (worker)
|
|
77
|
+
this.stopping = Promise.all([this.stopping, worker.terminate()]).then(() => { });
|
|
78
|
+
await this.stopping;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { DatabaseSync } from "node:sqlite";
|
|
2
|
+
import { parentPort, workerData } from "node:worker_threads";
|
|
3
|
+
import { searchMessagesInDatabase } from "./message-search-query.js";
|
|
4
|
+
// Each request reads the latest committed WAL state. Closing after the scan
|
|
5
|
+
// avoids keeping an old database inode open across a workspace restore.
|
|
6
|
+
parentPort.on("message", (request) => {
|
|
7
|
+
let db;
|
|
8
|
+
try {
|
|
9
|
+
db = new DatabaseSync(workerData.file, { readOnly: true });
|
|
10
|
+
const hits = searchMessagesInDatabase(db, request.query, request.limit, request.threadId);
|
|
11
|
+
parentPort.postMessage({ id: request.id, hits });
|
|
12
|
+
}
|
|
13
|
+
catch {
|
|
14
|
+
// Database errors can contain private paths or query text. The server
|
|
15
|
+
// returns a generic retryable error, never a synchronous fallback scan.
|
|
16
|
+
parentPort.postMessage({ id: request.id, failed: true });
|
|
17
|
+
}
|
|
18
|
+
finally {
|
|
19
|
+
db?.close();
|
|
20
|
+
}
|
|
21
|
+
});
|
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
import { harnessCapabilityLines } from "./harness-capabilities.js";
|
|
2
|
+
import { newId } from "./contracts.js";
|
|
3
|
+
import { redactSecretsInText } from "./redact.js";
|
|
4
|
+
const MAX_REASON = 500;
|
|
5
|
+
const STALE = "This bot's default model changed after this card was prepared. Ask the bot to review it and propose again.";
|
|
6
|
+
const NO_SUCH_BOT = "That bot no longer exists";
|
|
7
|
+
export class ModelRequestError extends Error {
|
|
8
|
+
status;
|
|
9
|
+
constructor(message, status = 400) {
|
|
10
|
+
super(message);
|
|
11
|
+
this.name = "ModelRequestError";
|
|
12
|
+
this.status = status;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
const sameSelection = (a, b) => a.instanceId === b.instanceId && a.model === b.model && a.effort === b.effort && a.variant === b.variant;
|
|
16
|
+
function reasonText(value) {
|
|
17
|
+
if (typeof value !== "string")
|
|
18
|
+
throw new ModelRequestError("reason is required");
|
|
19
|
+
const trimmed = value.trim();
|
|
20
|
+
if (!trimmed)
|
|
21
|
+
throw new ModelRequestError("reason is required");
|
|
22
|
+
if (trimmed.length > MAX_REASON)
|
|
23
|
+
throw new ModelRequestError("reason must be 500 characters or fewer");
|
|
24
|
+
return redactSecretsInText(trimmed);
|
|
25
|
+
}
|
|
26
|
+
/** Structural check only — the injected validateModel hook owns catalog and
|
|
27
|
+
* approval-compatibility checks with the live registry, at propose and again
|
|
28
|
+
* at confirm. */
|
|
29
|
+
function parseSelection(input) {
|
|
30
|
+
if (typeof input !== "object" || input === null || Array.isArray(input)) {
|
|
31
|
+
throw new ModelRequestError("model_selection must be an object with instanceId and model");
|
|
32
|
+
}
|
|
33
|
+
const value = input;
|
|
34
|
+
if (typeof value.instanceId !== "string" || !value.instanceId.trim()) {
|
|
35
|
+
throw new ModelRequestError("model_selection.instanceId is required");
|
|
36
|
+
}
|
|
37
|
+
if (typeof value.model !== "string" || !value.model.trim()) {
|
|
38
|
+
throw new ModelRequestError("model_selection.model is required");
|
|
39
|
+
}
|
|
40
|
+
const selection = { instanceId: value.instanceId.trim(), model: value.model.trim() };
|
|
41
|
+
if (value.effort !== undefined) {
|
|
42
|
+
if (typeof value.effort !== "string" || !value.effort.trim())
|
|
43
|
+
throw new ModelRequestError("model_selection.effort must be a level name");
|
|
44
|
+
selection.effort = value.effort;
|
|
45
|
+
}
|
|
46
|
+
if (value.variant !== undefined) {
|
|
47
|
+
if (typeof value.variant !== "string" || !value.variant.trim())
|
|
48
|
+
throw new ModelRequestError("model_selection.variant must be a variant id");
|
|
49
|
+
selection.variant = value.variant;
|
|
50
|
+
}
|
|
51
|
+
return selection;
|
|
52
|
+
}
|
|
53
|
+
export function modelCardCopy(args) {
|
|
54
|
+
const title = args.crossBot
|
|
55
|
+
? "Change @" + args.targetName + "'s default engine/model?"
|
|
56
|
+
: "Change " + args.targetName + "'s default engine/model?";
|
|
57
|
+
const lines = args.crossBot
|
|
58
|
+
? ["Whose default model: @" + args.targetName, "Why: " + args.reason]
|
|
59
|
+
: ["Why: " + args.reason];
|
|
60
|
+
lines.push("Default engine/model: " + JSON.stringify(args.before) + " → " + JSON.stringify(args.selection));
|
|
61
|
+
lines.push(...args.capabilityLines);
|
|
62
|
+
// The same consent line the team-setup card shows for a model change:
|
|
63
|
+
// the default moves, saved thread selections do not.
|
|
64
|
+
lines.push("Default models apply to groups and new threads. Every existing thread keeps its current model and permissions.");
|
|
65
|
+
lines.push("Nothing runs.");
|
|
66
|
+
return { title, summary: title + " · default engine/model", detail: lines.join("\n") };
|
|
67
|
+
}
|
|
68
|
+
export class ModelRequestService {
|
|
69
|
+
store;
|
|
70
|
+
now;
|
|
71
|
+
canPersist;
|
|
72
|
+
autoApply;
|
|
73
|
+
validateTarget;
|
|
74
|
+
validateModel;
|
|
75
|
+
driverCapabilities;
|
|
76
|
+
constructor(options) {
|
|
77
|
+
this.store = options.store;
|
|
78
|
+
this.now = options.now ?? Date.now;
|
|
79
|
+
this.canPersist = options.canPersist;
|
|
80
|
+
this.autoApply = options.autoApply;
|
|
81
|
+
this.validateTarget = options.validateTarget;
|
|
82
|
+
this.validateModel = options.validateModel;
|
|
83
|
+
this.driverCapabilities = options.driverCapabilities;
|
|
84
|
+
}
|
|
85
|
+
propose(args) {
|
|
86
|
+
return this.prepare(args);
|
|
87
|
+
}
|
|
88
|
+
submit(args) {
|
|
89
|
+
const proposal = this.prepare(args, true);
|
|
90
|
+
return { ...proposal, state: proposal.result ? "applied" : "pending" };
|
|
91
|
+
}
|
|
92
|
+
prepare(args, submitted = false) {
|
|
93
|
+
const reason = reasonText(args.reason);
|
|
94
|
+
const selection = parseSelection(args.selection);
|
|
95
|
+
const targetBotId = args.targetBotId?.trim() || args.botId;
|
|
96
|
+
const target = this.store.bot(targetBotId);
|
|
97
|
+
if (!target)
|
|
98
|
+
throw new ModelRequestError(NO_SUCH_BOT, 404);
|
|
99
|
+
const crossBot = targetBotId !== args.botId;
|
|
100
|
+
if (crossBot && this.validateTarget) {
|
|
101
|
+
const refusal = this.validateTarget(args.botId, targetBotId);
|
|
102
|
+
if (refusal)
|
|
103
|
+
throw new ModelRequestError(refusal, 403);
|
|
104
|
+
}
|
|
105
|
+
if (sameSelection(selection, target.modelSelection))
|
|
106
|
+
throw new ModelRequestError("Nothing would change");
|
|
107
|
+
if (this.validateModel) {
|
|
108
|
+
const refusal = this.validateModel(selection, target);
|
|
109
|
+
if (refusal)
|
|
110
|
+
throw new ModelRequestError(refusal, 400);
|
|
111
|
+
}
|
|
112
|
+
const requestId = newId();
|
|
113
|
+
const targetName = redactSecretsInText(target.name);
|
|
114
|
+
const before = structuredClone(target.modelSelection);
|
|
115
|
+
const capabilityLines = this.driverCapabilities
|
|
116
|
+
? harnessCapabilityLines(this.driverCapabilities(before.instanceId), this.driverCapabilities(selection.instanceId))
|
|
117
|
+
: [];
|
|
118
|
+
const payload = {
|
|
119
|
+
version: 1,
|
|
120
|
+
requestId,
|
|
121
|
+
botId: args.botId,
|
|
122
|
+
threadId: args.threadId,
|
|
123
|
+
targetBotId,
|
|
124
|
+
targetName,
|
|
125
|
+
createdAt: this.now(),
|
|
126
|
+
reason,
|
|
127
|
+
selection,
|
|
128
|
+
before,
|
|
129
|
+
};
|
|
130
|
+
const copy = modelCardCopy({ targetName, crossBot, before, selection, reason, capabilityLines });
|
|
131
|
+
const persistence = this.canPersist?.(args.botId, args.threadId);
|
|
132
|
+
if (persistence && !persistence.ok)
|
|
133
|
+
throw new ModelRequestError(persistence.error, persistence.status);
|
|
134
|
+
const automatic = submitted && this.autoApply?.(args.botId, args.threadId) === true;
|
|
135
|
+
const messageInput = {
|
|
136
|
+
role: "bot",
|
|
137
|
+
kind: "options",
|
|
138
|
+
card: {
|
|
139
|
+
title: copy.title,
|
|
140
|
+
subtitle: copy.detail,
|
|
141
|
+
options: automatic ? [] : ["Confirm", "Cancel"],
|
|
142
|
+
...(automatic ? { dismissed: true } : {}),
|
|
143
|
+
requestId,
|
|
144
|
+
tool: "update_model",
|
|
145
|
+
modelRequest: payload,
|
|
146
|
+
},
|
|
147
|
+
};
|
|
148
|
+
if (args.from)
|
|
149
|
+
messageInput.from = args.from;
|
|
150
|
+
const message = this.store.appendMessage(args.threadId, messageInput);
|
|
151
|
+
const proposal = { requestId, messageId: message.id, title: copy.title, summary: copy.summary, detail: copy.detail };
|
|
152
|
+
if (!automatic)
|
|
153
|
+
return proposal;
|
|
154
|
+
const result = this.resolve({ botId: args.botId, threadId: args.threadId, requestId, behavior: "allow" });
|
|
155
|
+
if (result.state === "applied")
|
|
156
|
+
return { ...proposal, result };
|
|
157
|
+
throw new ModelRequestError(result.state === "invalid" ? result.error : "The model change could not be applied", result.state === "invalid" ? result.status : 409);
|
|
158
|
+
}
|
|
159
|
+
/** Claims a model card even after it was settled, so a duplicate click
|
|
160
|
+
* never re-applies an already-applied change. */
|
|
161
|
+
resolve(args) {
|
|
162
|
+
const message = this.store
|
|
163
|
+
.messagesFor(args.threadId)
|
|
164
|
+
.find((candidate) => candidate.card?.requestId === args.requestId && candidate.card.modelRequest);
|
|
165
|
+
const card = message?.card;
|
|
166
|
+
const payload = card?.modelRequest;
|
|
167
|
+
if (!message || !card || !payload)
|
|
168
|
+
return { claimed: false, state: "not_found" };
|
|
169
|
+
if (payload.requestId !== card.requestId) {
|
|
170
|
+
return { claimed: true, state: "invalid", error: "This model request does not match its card", status: 409 };
|
|
171
|
+
}
|
|
172
|
+
if (args.behavior !== "allow" && args.behavior !== "deny") {
|
|
173
|
+
return { claimed: true, state: "invalid", error: "Model confirmations must be confirmed or cancelled", status: 400 };
|
|
174
|
+
}
|
|
175
|
+
if (payload.botId !== args.botId || payload.threadId !== args.threadId) {
|
|
176
|
+
return { claimed: true, state: "invalid", error: "This model request belongs to another conversation", status: 403 };
|
|
177
|
+
}
|
|
178
|
+
if (card.answered)
|
|
179
|
+
return { claimed: true, state: "already_settled", behavior: card.answered };
|
|
180
|
+
// Cross-bot cards re-check authority on every allow, before any settle
|
|
181
|
+
// path can run: a proposer who lost Chief rights must not confirm, even
|
|
182
|
+
// when the requested selection is already live. Card level, not inside
|
|
183
|
+
// the try, so the crash-recovery catch cannot recast a refusal as an
|
|
184
|
+
// applied change.
|
|
185
|
+
if (args.behavior === "allow" && payload.targetBotId !== payload.botId && this.validateTarget) {
|
|
186
|
+
const refusal = this.validateTarget(payload.botId, payload.targetBotId);
|
|
187
|
+
if (refusal)
|
|
188
|
+
return { claimed: true, state: "invalid", error: refusal, status: 404 };
|
|
189
|
+
}
|
|
190
|
+
try {
|
|
191
|
+
const target = this.store.bot(payload.targetBotId);
|
|
192
|
+
if (!target)
|
|
193
|
+
throw new ModelRequestError(NO_SUCH_BOT, 404);
|
|
194
|
+
if (args.behavior === "deny") {
|
|
195
|
+
this.store.patchMessage(args.threadId, message.id, { card: { ...card, answered: "deny", held: undefined } });
|
|
196
|
+
return { claimed: true, state: "denied" };
|
|
197
|
+
}
|
|
198
|
+
// Idempotent close: if the exact requested selection is already the
|
|
199
|
+
// default (a crash after apply, or the same change arriving twice),
|
|
200
|
+
// settle the card without mutating anything again.
|
|
201
|
+
if (sameSelection(payload.selection, target.modelSelection)) {
|
|
202
|
+
const settled = this.store.patchMessage(args.threadId, message.id, {
|
|
203
|
+
card: { ...card, answered: "allow", held: undefined, modelRequest: { ...payload, appliedAt: payload.appliedAt ?? this.now() } },
|
|
204
|
+
});
|
|
205
|
+
if (!settled)
|
|
206
|
+
throw new ModelRequestError("This model confirmation card is no longer available", 409);
|
|
207
|
+
return { claimed: true, state: "already_settled", behavior: "allow" };
|
|
208
|
+
}
|
|
209
|
+
if (!sameSelection(payload.before, target.modelSelection)) {
|
|
210
|
+
throw new ModelRequestError(STALE, 409);
|
|
211
|
+
}
|
|
212
|
+
if (this.validateModel) {
|
|
213
|
+
const refusal = this.validateModel(payload.selection, target);
|
|
214
|
+
if (refusal)
|
|
215
|
+
throw new ModelRequestError(refusal, 409);
|
|
216
|
+
}
|
|
217
|
+
if (!this.store.applyModelDefault(target.id, payload.selection))
|
|
218
|
+
throw new ModelRequestError(NO_SUCH_BOT, 404);
|
|
219
|
+
const appliedAt = this.now();
|
|
220
|
+
const settled = this.store.patchMessage(args.threadId, message.id, {
|
|
221
|
+
card: { ...card, answered: "allow", held: undefined, modelRequest: { ...payload, appliedAt } },
|
|
222
|
+
});
|
|
223
|
+
if (!settled)
|
|
224
|
+
throw new ModelRequestError("This model confirmation card is no longer available", 409);
|
|
225
|
+
return { claimed: true, state: "applied", targetBotId: target.id };
|
|
226
|
+
}
|
|
227
|
+
catch (error) {
|
|
228
|
+
const status = error instanceof ModelRequestError ? error.status : 400;
|
|
229
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
230
|
+
const applied = sameSelection(payload.selection, this.store.bot(payload.targetBotId)?.modelSelection ?? payload.before);
|
|
231
|
+
const notice = card.dismissed && card.options.length === 0
|
|
232
|
+
? "Default model saved. Recording the operation receipt could not finish; the change will not be applied again."
|
|
233
|
+
: "Default model saved. Confirm again to finish recording this decision; the change will not be applied again.";
|
|
234
|
+
try {
|
|
235
|
+
this.store.patchMessage(args.threadId, message.id, {
|
|
236
|
+
card: { ...card, held: applied ? notice : redactSecretsInText(detail).slice(0, 500) },
|
|
237
|
+
});
|
|
238
|
+
}
|
|
239
|
+
catch { /* The idempotent close above still permits a safe retry. */ }
|
|
240
|
+
if (applied)
|
|
241
|
+
return {
|
|
242
|
+
claimed: true, state: "applied", targetBotId: payload.targetBotId,
|
|
243
|
+
settlementPending: true, message: notice,
|
|
244
|
+
};
|
|
245
|
+
return { claimed: true, state: "invalid", error: detail, status };
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { parseOptionsCardInput, WATCHER_OPTIONS_CARD_BOT_ID, } from "../shared/options-card.js";
|
|
2
|
+
/**
|
|
3
|
+
* Persist one passive, legacy options card. It deliberately has no requestId
|
|
4
|
+
* or tool field: clicking it replies to the conversation, but grants no tool
|
|
5
|
+
* approval and performs no external action.
|
|
6
|
+
*/
|
|
7
|
+
export function createOptionsCard(args) {
|
|
8
|
+
if (args.bot.id !== WATCHER_OPTIONS_CARD_BOT_ID) {
|
|
9
|
+
return { ok: false, status: 403, error: "create_options_card is not enabled for this bot." };
|
|
10
|
+
}
|
|
11
|
+
const parsed = parseOptionsCardInput(args.input);
|
|
12
|
+
if (!parsed.ok)
|
|
13
|
+
return { ok: false, status: 400, error: parsed.error };
|
|
14
|
+
const message = args.store.appendMessage(args.threadId, {
|
|
15
|
+
role: "bot",
|
|
16
|
+
kind: "options",
|
|
17
|
+
from: { botId: args.bot.id, name: args.bot.name, color: args.bot.color },
|
|
18
|
+
card: parsed.value,
|
|
19
|
+
});
|
|
20
|
+
return { ok: true, messageId: message.id };
|
|
21
|
+
}
|
|
@@ -258,6 +258,14 @@ export class OrgLibrary {
|
|
|
258
258
|
get organization() {
|
|
259
259
|
return this.library ? { ...this.library.catalog.organization } : null;
|
|
260
260
|
}
|
|
261
|
+
/** Every install's status, from this library's own state (what it last
|
|
262
|
+
* rebuilt, added or withdrew, saved or not), for New bot's presets
|
|
263
|
+
* (presets.ts presetOffered). Every install is listed, whichever
|
|
264
|
+
* organization it came from and with no organization signed in, as the
|
|
265
|
+
* statuses have always been read. Reads no file and no skill state. */
|
|
266
|
+
installStatuses() {
|
|
267
|
+
return new Map(Object.entries(this.state.installs).map(([installId, install]) => [installId, install.status]));
|
|
268
|
+
}
|
|
261
269
|
// ── the relay (Electron main → runtime) ───────────────────────────────
|
|
262
270
|
/** `openmausbot:managed-library`: swap the catalog and return at once; the
|
|
263
271
|
* reconcile and the report run afterwards (the relay times out at 15 s).
|
|
@@ -340,6 +348,10 @@ export class OrgLibrary {
|
|
|
340
348
|
value.sections.push(sectionKey(bot.section));
|
|
341
349
|
value.meta ??= stamp;
|
|
342
350
|
}
|
|
351
|
+
// A bot made from an install's preset (presets.ts) is the person's own,
|
|
352
|
+
// like a copy: its stamped skills never count as the install's records.
|
|
353
|
+
if (stamp?.presetKey)
|
|
354
|
+
continue;
|
|
343
355
|
for (const skill of this.deps.skills.stamps(bot.id))
|
|
344
356
|
entry(skill.stamp.installId).skills += 1;
|
|
345
357
|
}
|
|
@@ -598,7 +610,9 @@ export class OrgLibrary {
|
|
|
598
610
|
}
|
|
599
611
|
}
|
|
600
612
|
// A bot made from one of the install's presets (presets.ts) got the
|
|
601
|
-
// preset's skills under the release's organization source
|
|
613
|
+
// preset's skills under the release's organization source. They carry
|
|
614
|
+
// the install's stamp too (`via: "preset"`, reached above); the source
|
|
615
|
+
// also reaches one made before presets stamped their skills.
|
|
602
616
|
for (const bot of this.deps.store.bots) {
|
|
603
617
|
const made = bot.installedPackage;
|
|
604
618
|
if (made?.source !== "org" || made.installId !== installId || !made.presetKey || !made.ref)
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
// Delivery receipts for bot-to-bot messages: what actually happened to a
|
|
2
|
+
// send the moment the coordination/delegation tool returned, one receipt
|
|
3
|
+
// per recipient. The outcomes are the bot-facing half of the same honesty
|
|
4
|
+
// discipline SteerOutcome applies to steering — a tri-state that can always
|
|
5
|
+
// be answered truthfully, where an indeterminate send reads "queued", never
|
|
6
|
+
// "delivered":
|
|
7
|
+
//
|
|
8
|
+
// queued — accepted into a durable lane; the recipient's turn has not
|
|
9
|
+
// started, so nothing has been delivered yet
|
|
10
|
+
// injected — the recipient's turn started with this message (it may still
|
|
11
|
+
// be running, or it may have run and failed)
|
|
12
|
+
// failed — not delivered; the detail says why
|
|
13
|
+
//
|
|
14
|
+
// The rule that overrides every other consideration: never report delivery
|
|
15
|
+
// that did not happen. A failed live hand-off may stay buffered for a later
|
|
16
|
+
// drain, but its receipt stays failed.
|
|
17
|
+
import { peerName } from "./peer-roster.js";
|
|
18
|
+
// Detail lines are built from harness error text that can carry
|
|
19
|
+
// peer- or provider-authored content, so they get the same flatten-and-clip
|
|
20
|
+
// discipline as a roster entry (peer-roster.ts) rather than trust the
|
|
21
|
+
// sender of the failure. Written as a scan for the same reason the linter
|
|
22
|
+
// refuses a control-character literal.
|
|
23
|
+
const DETAIL_MAX = 200;
|
|
24
|
+
const oneLine = (value) => {
|
|
25
|
+
let flattened = "";
|
|
26
|
+
for (let i = 0; i < value.length; i += 1) {
|
|
27
|
+
const code = value.charCodeAt(i);
|
|
28
|
+
const breaksOut = code < 0x20 || (code >= 0x7f && code <= 0x9f) || code === 0x2028 || code === 0x2029;
|
|
29
|
+
flattened += breaksOut ? " " : value[i];
|
|
30
|
+
}
|
|
31
|
+
return flattened.replace(/\s+/g, " ").trim();
|
|
32
|
+
};
|
|
33
|
+
const clip = (value) => {
|
|
34
|
+
const flat = oneLine(value);
|
|
35
|
+
return flat.length > DETAIL_MAX ? `${flat.slice(0, DETAIL_MAX - 1)}…` : flat;
|
|
36
|
+
};
|
|
37
|
+
/** Build a receipt with the detail flattened onto one clipped line. */
|
|
38
|
+
export function peerDeliveryReceipt(receipt) {
|
|
39
|
+
return {
|
|
40
|
+
botId: receipt.botId,
|
|
41
|
+
...(receipt.botName ? { botName: receipt.botName } : {}),
|
|
42
|
+
outcome: receipt.outcome,
|
|
43
|
+
detail: clip(receipt.detail) || "no detail recorded",
|
|
44
|
+
...(receipt.taskId ? { taskId: receipt.taskId } : {}),
|
|
45
|
+
...(receipt.requestId ? { requestId: receipt.requestId } : {}),
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
const named = (receipt) => peerName(receipt.botName ?? "") || peerName(receipt.botId) || "teammate";
|
|
49
|
+
/** Render receipts as the tool-result prose a sender reads. One receipt
|
|
50
|
+
* becomes a single "Delivery to …" line; several become a fenced list with
|
|
51
|
+
* one line per recipient, in the order the caller addressed them. An empty
|
|
52
|
+
* list renders nothing, so callers can append unconditionally. */
|
|
53
|
+
export function renderPeerDeliveryReceipts(receipts) {
|
|
54
|
+
if (!receipts.length)
|
|
55
|
+
return "";
|
|
56
|
+
const line = (receipt) => `${named(receipt)}: ${receipt.outcome} — ${receipt.detail}`;
|
|
57
|
+
if (receipts.length === 1)
|
|
58
|
+
return `Delivery to ${line(receipts[0])}`;
|
|
59
|
+
return ["Delivery receipts:", ...receipts.map(receipt => `- ${line(receipt)}`)].join("\n");
|
|
60
|
+
}
|