openmausbot 0.1.86 → 0.1.88
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 +4 -3
- package/dist/assets/{abnfDiagram-O67JEVCF-DpUZNnFD.js → abnfDiagram-O67JEVCF-qd9VD3GV.js} +1 -1
- package/dist/assets/{arc-DW7mrI4p.js → arc-3RwkUjRe.js} +1 -1
- package/dist/assets/{architectureDiagram-NJMV4G6O-CyeZ5WLZ.js → architectureDiagram-NJMV4G6O-CFKSJS7a.js} +1 -1
- package/dist/assets/{blockDiagram-BEXU5L5S-CBVaNjO7.js → blockDiagram-BEXU5L5S-CG1rEfWD.js} +1 -1
- package/dist/assets/{c4Diagram-YGBWAQC7-sf8d41uT.js → c4Diagram-YGBWAQC7-CBW9N664.js} +1 -1
- package/dist/assets/channel-CnRnO3tF.js +1 -0
- package/dist/assets/{chunk-2Q5K7J3B-BOGvy7Y_.js → chunk-2Q5K7J3B-2mRfTksB.js} +1 -1
- package/dist/assets/{chunk-3FUC2YCW-BiF9zyiz.js → chunk-3FUC2YCW-Cv1dpErj.js} +2 -2
- package/dist/assets/{chunk-5VM5RSS4-EFuZ1SBZ.js → chunk-5VM5RSS4-B7IACJp-.js} +1 -1
- package/dist/assets/{chunk-GTNCS2PH-pJLNSAT7.js → chunk-GTNCS2PH-An0hZtYa.js} +1 -1
- package/dist/assets/{chunk-GWA4HPMP-Cb-3_3IT.js → chunk-GWA4HPMP-BcJB2nEm.js} +1 -1
- package/dist/assets/{chunk-JWPE2WC7-DAf5aogn.js → chunk-JWPE2WC7-CV_W7TN7.js} +1 -1
- package/dist/assets/{chunk-NETBCI7D-DR3kiwZz.js → chunk-NETBCI7D-Bcs_kwnA.js} +1 -1
- package/dist/assets/{chunk-WEXAMYUT-BmU8PeyM.js → chunk-WEXAMYUT-lrx_DxFP.js} +1 -1
- package/dist/assets/{chunk-XXDRQBXY-Buh8g8OT.js → chunk-XXDRQBXY-DabHlLGM.js} +1 -1
- package/dist/assets/{classDiagram-v2-NBCMYWYE-BurfmMWM.js → classDiagram-v2-NBCMYWYE-BnHj6P7d.js} +1 -1
- package/dist/assets/{cose-bilkent-JH36ORCC-KkGZUEkU.js → cose-bilkent-JH36ORCC-CfLp6UDi.js} +1 -1
- package/dist/assets/{cynefin-OW5HDTMX-C3PpV96d.js → cynefin-OW5HDTMX-Dm4dbVA1.js} +1 -1
- package/dist/assets/{cynefinDiagram-VND7K2PF-Dt8JwJaI.js → cynefinDiagram-VND7K2PF-B9mH-PID.js} +1 -1
- package/dist/assets/{dagre-6A5THRUB-DkjN8266.js → dagre-6A5THRUB-DDc_NVQJ.js} +1 -1
- package/dist/assets/{diagram-22UHCM2B-DUAJS0JV.js → diagram-22UHCM2B-lwr8Ip8-.js} +1 -1
- package/dist/assets/{diagram-3UASUU5V-Dw4KhF1a.js → diagram-3UASUU5V-BLLe6dmI.js} +1 -1
- package/dist/assets/{diagram-ATOU4E4O-Dd1gmQqf.js → diagram-ATOU4E4O-6eUIq4lz.js} +1 -1
- package/dist/assets/{diagram-CDSNMT55-CjwGs0Yw.js → diagram-CDSNMT55-O-pRN7VQ.js} +1 -1
- package/dist/assets/{diagram-MLGK6HIB-BL-69Zrq.js → diagram-MLGK6HIB-BietvGAI.js} +1 -1
- package/dist/assets/{diagram-MPIPVDR6-5Zi2N7wo.js → diagram-MPIPVDR6-Bd2Cl62X.js} +1 -1
- package/dist/assets/{ebnfDiagram-ZINNZB2B-D9NekWUX.js → ebnfDiagram-ZINNZB2B-B6iDy69W.js} +1 -1
- package/dist/assets/{elk-276RUBZZ-CkaSQjxw.js → elk-276RUBZZ-DQVHfKe0.js} +1 -1
- package/dist/assets/{erDiagram-OPXOYQCR-CdAVBV8-.js → erDiagram-OPXOYQCR-BWdVp2tQ.js} +1 -1
- package/dist/assets/{flowDiagram-KWPJA3E3-Bm_lWWH7.js → flowDiagram-KWPJA3E3-WLE6rKB4.js} +1 -1
- package/dist/assets/{ganttDiagram-FUAMR5RP-FZ8ejYWJ.js → ganttDiagram-FUAMR5RP-BbjS-4gn.js} +1 -1
- package/dist/assets/{gitGraphDiagram-X574FWY7-Dlv9rizZ.js → gitGraphDiagram-X574FWY7-Dpy2QSGq.js} +1 -1
- package/dist/assets/index-BIMdIbaw.css +1 -0
- package/dist/assets/index-CHnODCr2.js +583 -0
- package/dist/assets/{index-h9FFNwTu.js → index-CIaHn5NS.js} +1 -1
- package/dist/assets/{infoDiagram-VRGFBTTK-BdQ_gW5B.js → infoDiagram-VRGFBTTK-D_jKWlKe.js} +1 -1
- package/dist/assets/{ishikawaDiagram-OU5B5YK6-EwssEp2X.js → ishikawaDiagram-OU5B5YK6-DLJohSuK.js} +1 -1
- package/dist/assets/{journeyDiagram-ZHPQQLJL-BX1G1vOd.js → journeyDiagram-ZHPQQLJL-DPOFj0qN.js} +1 -1
- package/dist/assets/{kanban-definition-PNTS6WVX-BB4kAsyC.js → kanban-definition-PNTS6WVX-BI7v-xpK.js} +1 -1
- package/dist/assets/{linear-3su2TG93.js → linear-C2pWPSxO.js} +1 -1
- package/dist/assets/{mermaid.core-CDD1E0s9.js → mermaid.core-CR-j5WQU.js} +5 -5
- package/dist/assets/{mindmap-definition-NLK3R4M7-Ba50MsNK.js → mindmap-definition-NLK3R4M7-lz9GvXRh.js} +1 -1
- package/dist/assets/{pegDiagram-GJSIUBJH-DZLkkZlM.js → pegDiagram-GJSIUBJH-Dl4_ia61.js} +1 -1
- package/dist/assets/{pieDiagram-5QR66LMP-DaTz77Uq.js → pieDiagram-5QR66LMP-DWuT3n88.js} +1 -1
- package/dist/assets/{quadrantDiagram-O4NWA36T-CpaiidpR.js → quadrantDiagram-O4NWA36T-DXWe9Jx7.js} +1 -1
- package/dist/assets/{railroadDiagram-XR7U4H2S-mN4JrMeI.js → railroadDiagram-XR7U4H2S-COY1IBVY.js} +1 -1
- package/dist/assets/{requirementDiagram-PLB6GJNP-BSNDRpg7.js → requirementDiagram-PLB6GJNP-DnyVuhsQ.js} +1 -1
- package/dist/assets/{sankeyDiagram-IPEJSGJF-FngzGNug.js → sankeyDiagram-IPEJSGJF-BSc4NdGK.js} +1 -1
- package/dist/assets/{sequenceDiagram-PO4LG4MO-CM4pCC3L.js → sequenceDiagram-PO4LG4MO-kqEU2EGq.js} +1 -1
- package/dist/assets/{sizeCapture-INFHLROL-CRICHXkw.js → sizeCapture-INFHLROL-BvB4bkWD.js} +1 -1
- package/dist/assets/{stateDiagram-v2-GCMORJYK-oyTWpxH2.js → stateDiagram-v2-GCMORJYK-BIrqNv7y.js} +1 -1
- package/dist/assets/{swimlanes-2SLR337P-D9h9mPBq.js → swimlanes-2SLR337P-CFnalQUE.js} +1 -1
- package/dist/assets/swimlanesDiagram-TC7HE7FX-C4a8qNGj.js +8 -0
- package/dist/assets/{timeline-definition-EJHVYXUP-BJUlN9bW.js → timeline-definition-EJHVYXUP-CsSWJnSQ.js} +1 -1
- package/dist/assets/{usecaseDiagram-POWQR4AR-3uBNkbAb.js → usecaseDiagram-POWQR4AR-DkAmJ1EE.js} +1 -1
- package/dist/assets/{vennDiagram-UO4OBE2U-D2odalzT.js → vennDiagram-UO4OBE2U-C3vd9drL.js} +1 -1
- package/dist/assets/{wardleyDiagram-VNRHLVJA-DHgFaB9S.js → wardleyDiagram-VNRHLVJA-j26CM-pi.js} +1 -1
- package/dist/assets/{xychartDiagram-PMCCYNJV-w5GYM2h0.js → xychartDiagram-PMCCYNJV-BYu1Q_at.js} +1 -1
- package/dist/index.html +2 -2
- package/dist-server/browser-proxy.js +2 -1
- package/dist-server/companion/src/routes.js +8 -1
- package/dist-server/connector-proxy.js +105 -1
- package/dist-server/container-mcp.js +1032 -165
- package/dist-server/drivers/agents-proxy.js +338 -29
- package/dist-server/drivers/phone-proxy.js +33 -2
- package/dist-server/index.js +40590 -31811
- package/dist-server/local-computer-proxy.js +5 -3
- package/dist-server/local-computer.js +53 -17
- 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 +4457 -1399
- package/dist-server/pair-cli.js +4457 -1399
- package/dist-server/permission-proxy.js +29 -9
- package/dist-server/server/admission.js +81 -0
- package/dist-server/server/aside-queue.js +294 -0
- package/dist-server/server/attachments.js +98 -1
- package/dist-server/server/auto-approve.js +4 -3
- package/dist-server/server/auto-vm-claims.js +1 -0
- package/dist-server/server/bot-attachment.js +107 -0
- package/dist-server/server/bot-overview.js +19 -0
- package/dist-server/server/bot-package.js +18 -292
- package/dist-server/server/bot-profile.js +5 -17
- package/dist-server/server/box-create-idempotency.js +3 -3
- package/dist-server/server/box.js +57 -55
- package/dist-server/server/browser-proxy.js +3 -2
- package/dist-server/server/browser-runtime.js +37 -1
- package/dist-server/server/cli-api-setup.js +1 -0
- package/dist-server/server/cli.js +2 -2
- package/dist-server/server/command-allowlist.js +154 -0
- package/dist-server/server/composio.js +297 -1
- package/dist-server/server/computer-wait.js +22 -9
- package/dist-server/server/config.js +84 -12
- 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 +168 -0
- package/dist-server/server/container-computer.js +61 -1
- package/dist-server/server/contracts.js +22 -0
- package/dist-server/server/default-model-selection.js +9 -0
- package/dist-server/server/delegations.js +85 -34
- package/dist-server/server/drivers/acp/core.js +80 -20
- 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 +9 -15
- package/dist-server/server/drivers/agents-call.js +179 -13
- package/dist-server/server/drivers/agents-catalog.js +134 -10
- package/dist-server/server/drivers/boxagent.js +160 -36
- package/dist-server/server/drivers/builtIn.js +2 -0
- package/dist-server/server/drivers/chat-box-tools.js +90 -0
- package/dist-server/server/drivers/chat-images.js +54 -0
- package/dist-server/server/drivers/chat-mcp-tools.js +38 -8
- package/dist-server/server/drivers/chat-tool-approval.js +70 -18
- package/dist-server/server/drivers/claude.js +38 -19
- 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-instructions.js +48 -6
- package/dist-server/server/drivers/codex-release.js +48 -0
- package/dist-server/server/drivers/codex-tool-surface.js +20 -0
- package/dist-server/server/drivers/codex.js +102 -55
- package/dist-server/server/drivers/mistral.js +115 -0
- package/dist-server/server/drivers/openai-chat.js +180 -33
- package/dist-server/server/drivers/openai-compat.js +1 -0
- package/dist-server/server/drivers/permission-command.js +35 -0
- package/dist-server/server/drivers/phone-proxy.js +47 -2
- package/dist-server/server/drivers/pi.js +306 -28
- package/dist-server/server/drivers/prompt-split.js +110 -0
- package/dist-server/server/drivers/session-idle.js +29 -0
- package/dist-server/server/env-path.js +15 -0
- package/dist-server/server/fleet.js +1 -1
- package/dist-server/server/group-thread-usage.js +103 -0
- package/dist-server/server/guarded-requests.js +2 -2
- package/dist-server/server/harness/registry.js +1 -0
- package/dist-server/server/harness-capabilities.js +46 -0
- package/dist-server/server/hosted-models.js +1 -1
- package/dist-server/server/index.js +2396 -505
- package/dist-server/server/local-computer.js +72 -23
- package/dist-server/server/managed-desktop.js +1 -1
- package/dist-server/server/mcp-bridge.js +6 -6
- 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/new-bot-defaults.js +115 -0
- package/dist-server/server/options-card.js +21 -0
- package/dist-server/server/org-library.js +1065 -0
- package/dist-server/server/package-export.js +627 -39
- package/dist-server/server/package-import.js +458 -0
- package/dist-server/server/package-parts.js +87 -0
- package/dist-server/server/peer-approval.js +16 -7
- package/dist-server/server/peer-delivery.js +60 -0
- package/dist-server/server/peer-roster.js +67 -0
- package/dist-server/server/presets.js +492 -0
- 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/provider-key-check.js +3 -2
- package/dist-server/server/published-teams.js +80 -0
- package/dist-server/server/room-handoffs.js +19 -3
- package/dist-server/server/routes/bot-presets.js +25 -0
- package/dist-server/server/routine-requests.js +35 -10
- package/dist-server/server/routines.js +41 -1
- package/dist-server/server/section-context.js +10 -0
- package/dist-server/server/skill-fetch.js +72 -14
- package/dist-server/server/skills.js +35 -61
- package/dist-server/server/steer-queue.js +3 -2
- package/dist-server/server/store.js +291 -17
- package/dist-server/server/surface.js +10 -4
- package/dist-server/server/system-prompt.js +38 -6
- package/dist-server/server/team-backup.js +6 -2
- package/dist-server/server/team-computers.js +10 -0
- package/dist-server/server/team-library.js +6 -2
- package/dist-server/server/team-manifest.js +4 -3
- package/dist-server/server/team-setup-requests.js +139 -27
- package/dist-server/server/thread-events.js +20 -2
- package/dist-server/server/tightening-requests.js +405 -0
- package/dist-server/server/tts/chatterbox.js +27 -5
- package/dist-server/server/turn-context.js +2 -0
- package/dist-server/server/turn-watchdog.js +12 -3
- package/dist-server/server/usage-ledger.js +19 -2
- package/dist-server/server/vps-computer.js +139 -51
- package/dist-server/server/vps-container-mcp.js +7 -0
- package/dist-server/server/vps-ssh.js +36 -7
- package/dist-server/server/workspace-backup-policy.js +12 -0
- package/dist-server/server/workspace-backup.js +42 -5
- package/dist-server/server/workspace-backup.worker.js +10 -0
- package/dist-server/server/workspace.js +1 -1
- package/dist-server/shared/approval-mode.js +1 -1
- package/dist-server/shared/ask-question.js +207 -12
- package/dist-server/shared/bot-profile.js +15 -0
- package/dist-server/shared/command-allowlist.js +1 -0
- package/dist-server/shared/credential-request.js +6 -2
- 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/package-format.js +967 -0
- package/dist-server/shared/profile-request.js +5 -4
- package/dist-server/shared/skill-md.js +64 -0
- package/dist-server/shared/team-backup.js +7 -0
- package/dist-server/shared/tightening-request.js +87 -0
- package/dist-server/shared/wire.js +4 -0
- package/dist-server/src/lib/composer-attachments.js +25 -3
- package/dist-server/vps-container-mcp.js +1132 -171
- package/dist-server/workspace-backup.worker.js +19474 -0
- package/enterprise/README.md +1 -1
- package/package.json +1 -1
- package/dist/assets/channel-BXFkgus9.js +0 -1
- package/dist/assets/index-BGgqlWPJ.css +0 -1
- package/dist/assets/index-BtHo8ZKl.js +0 -577
- package/dist/assets/swimlanesDiagram-TC7HE7FX-BWrvB2ak.js +0 -8
|
@@ -8,10 +8,11 @@ var MAX_OPTIONS = 12;
|
|
|
8
8
|
var MAX_QUESTION_TEXT = 400;
|
|
9
9
|
var MAX_LABEL = 120;
|
|
10
10
|
var MAX_DESCRIPTION = 400;
|
|
11
|
+
var MAX_CUSTOM_ANSWER = 2e3;
|
|
11
12
|
function text(value, limit) {
|
|
12
13
|
if (typeof value !== "string") return void 0;
|
|
13
14
|
const trimmed = value.trim();
|
|
14
|
-
return trimmed ? trimmed.slice(0, limit) : void 0;
|
|
15
|
+
return trimmed ? trimmed.slice(0, limit).trim() : void 0;
|
|
15
16
|
}
|
|
16
17
|
function isRecord(value) {
|
|
17
18
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
@@ -59,19 +60,38 @@ function parseAskQuestions(input) {
|
|
|
59
60
|
}
|
|
60
61
|
return questions.length ? questions : null;
|
|
61
62
|
}
|
|
63
|
+
var OMB_ASK_FENCE = /(^|\n)[ \t]{0,3}(`{3,}|~{3,})[ \t]*omb-ask[ \t]*\r?\n([\s\S]*?)\r?\n[ \t]{0,3}\2[ \t]*(?=\r?\n|$)/;
|
|
64
|
+
var OMB_ASK_FENCE_GLOBAL = new RegExp(OMB_ASK_FENCE.source, OMB_ASK_FENCE.flags + "g");
|
|
65
|
+
var MAX_ANSWER_ECHO = 6 * MAX_CUSTOM_ANSWER;
|
|
62
66
|
function questionAnswersByQuestion(message, questions) {
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
67
|
+
return questionAnswersById(message, questions.map((question) => ({ id: question.question, question })));
|
|
68
|
+
}
|
|
69
|
+
function questionAnswersById(message, questions) {
|
|
70
|
+
const answers = /* @__PURE__ */ Object.create(null);
|
|
71
|
+
const idByText = /* @__PURE__ */ new Map();
|
|
72
|
+
const ambiguous = /* @__PURE__ */ new Set();
|
|
73
|
+
for (const { id, question } of questions) {
|
|
74
|
+
if (idByText.has(question.question)) {
|
|
75
|
+
ambiguous.add(question.question);
|
|
76
|
+
continue;
|
|
77
|
+
}
|
|
78
|
+
idByText.set(question.question, id);
|
|
79
|
+
}
|
|
80
|
+
const blocks = message.matchAll(/(?:^|\n\n)Q: ([\s\S]+?)\nA: ([\s\S]*?)(?=\n\nQ: |$)/g);
|
|
81
|
+
let sawBlock = false;
|
|
82
|
+
for (const match of blocks) {
|
|
83
|
+
sawBlock = true;
|
|
84
|
+
const asked = match[1].trim();
|
|
85
|
+
if (ambiguous.has(asked)) continue;
|
|
86
|
+
const id = idByText.get(asked);
|
|
87
|
+
const answer = match[2].trim();
|
|
88
|
+
if (id && answer) answers[id] = answer;
|
|
70
89
|
}
|
|
71
90
|
if (Object.keys(answers).length) return answers;
|
|
91
|
+
if (sawBlock) return {};
|
|
72
92
|
const only = questions.length === 1 ? questions[0] : void 0;
|
|
73
93
|
const flat = message.trim();
|
|
74
|
-
return only && flat ? { [only.
|
|
94
|
+
return only && flat ? { [only.id]: flat } : {};
|
|
75
95
|
}
|
|
76
96
|
|
|
77
97
|
// server/permission-proxy.ts
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
// One admission decision for every surface that starts, steers, or parks
|
|
2
|
+
// work. Each seam used to hand-roll its own busy policy; they agreed by
|
|
3
|
+
// convention and drifted by accident. This module is the frozen record of
|
|
4
|
+
// what each surface does today (M1): same inputs, same outputs, one place.
|
|
5
|
+
//
|
|
6
|
+
// Purity is the contract: no IO, no store reads, no adapter calls. Callers
|
|
7
|
+
// gather state, call admit(), and apply the returned decision with their own
|
|
8
|
+
// side effects. Anything that can settle a turn (an awaited steer, a slow
|
|
9
|
+
// disk) belongs to the caller, which re-reads state and asks again.
|
|
10
|
+
/** The shared admission decision. Pure: same surface, message, and state
|
|
11
|
+
* always return the same decision, with no side effects. */
|
|
12
|
+
export function admit(surface, message = {}, state = {}) {
|
|
13
|
+
switch (surface) {
|
|
14
|
+
case "direct-busy": {
|
|
15
|
+
// Steer-first for plain text into a capable engine; every mechanical
|
|
16
|
+
// clamp (attachments, a pending surface choice, an incapable engine)
|
|
17
|
+
// parks the words for the next ordinary turn instead.
|
|
18
|
+
const steerable = !message.carriesImages && !message.pendingComputerSelection && message.engineCanSteer === true;
|
|
19
|
+
return steerable ? { action: "steer" } : { action: "queue" };
|
|
20
|
+
}
|
|
21
|
+
case "direct": {
|
|
22
|
+
if (state.atCapacity || state.threadBusy || state.groupTurn || state.parksBehindCoordination) {
|
|
23
|
+
// Capacity outranks a room turn in the receipt because the person
|
|
24
|
+
// can fix it (finish a thread); coordination parking is reasonless.
|
|
25
|
+
const reason = state.atCapacity ? "capacity" : state.groupTurn ? "group-turn" : undefined;
|
|
26
|
+
return { action: "queue", reason };
|
|
27
|
+
}
|
|
28
|
+
return { action: "start" };
|
|
29
|
+
}
|
|
30
|
+
case "room":
|
|
31
|
+
// Room sends never auto-steer; the queue is the policy. Human override
|
|
32
|
+
// is the head-only manual steer below.
|
|
33
|
+
return state.roomWorking ? { action: "queue" } : { action: "start" };
|
|
34
|
+
case "room-steer": {
|
|
35
|
+
if (!message.speakerPresent || message.engineCanSteer !== true) {
|
|
36
|
+
// An incapable room keeps its queue, exactly like an incapable 1:1
|
|
37
|
+
// engine; the request succeeds without delivering.
|
|
38
|
+
return { action: "queue" };
|
|
39
|
+
}
|
|
40
|
+
if (!message.isQueueHead) {
|
|
41
|
+
return { action: "refuse", code: "queue-head-only" };
|
|
42
|
+
}
|
|
43
|
+
return { action: "steer" };
|
|
44
|
+
}
|
|
45
|
+
case "guarded": {
|
|
46
|
+
// A guarded send is an external contract: never queued, never steered,
|
|
47
|
+
// never started under stale permissions. Any busy shape refuses.
|
|
48
|
+
if (state.botBusy || state.threadBusy || state.atCapacity || state.parksBehindCoordination || state.groupTurn) {
|
|
49
|
+
return { action: "refuse", code: "guarded_busy" };
|
|
50
|
+
}
|
|
51
|
+
return { action: "start" };
|
|
52
|
+
}
|
|
53
|
+
case "unattended": {
|
|
54
|
+
// Routines and webhooks share startTurn's admission; busy means a
|
|
55
|
+
// missed or failed run, not a queue — their receipts are the
|
|
56
|
+
// monitoring feature.
|
|
57
|
+
if (state.botPresent === false)
|
|
58
|
+
return { action: "refuse", code: "missing" };
|
|
59
|
+
if (state.atCapacity || state.groupTurn)
|
|
60
|
+
return { action: "refuse", code: "busy" };
|
|
61
|
+
return { action: "start" };
|
|
62
|
+
}
|
|
63
|
+
case "peer": {
|
|
64
|
+
// Exactly what startTurn admits for a direct thread: the landing
|
|
65
|
+
// thread free, a free slot, no live room turn. Never the whole-bot
|
|
66
|
+
// busy flag.
|
|
67
|
+
if (state.threadBusy || state.atCapacity || state.groupTurn)
|
|
68
|
+
return { action: "refuse", code: "busy" };
|
|
69
|
+
return { action: "start" };
|
|
70
|
+
}
|
|
71
|
+
case "opened-thread": {
|
|
72
|
+
// A self-opened thread is brand new: its own busy flag is impossible,
|
|
73
|
+
// so only a full slot list or a live room turn can park it. Provenance
|
|
74
|
+
// (peerAsk, unattended) survives the wait in the queue payload.
|
|
75
|
+
if (state.atCapacity || state.groupTurn) {
|
|
76
|
+
return { action: "queue", reason: state.atCapacity ? "capacity" : "group-turn" };
|
|
77
|
+
}
|
|
78
|
+
return { action: "start" };
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
@@ -0,0 +1,294 @@
|
|
|
1
|
+
// The aside lane: peer context handed to a busy thread's RUNNING turn.
|
|
2
|
+
//
|
|
3
|
+
// A peer send that arrives mid-turn used to degrade straight to the
|
|
4
|
+
// delegation queue — full work, later turn, no reply until then. When the
|
|
5
|
+
// engine running that turn has a mid-turn seam (Adapter.steer), the same
|
|
6
|
+
// words can instead be folded into the live turn at its next step boundary
|
|
7
|
+
// as an ASIDE: context the model reads and usually ignores. The person's
|
|
8
|
+
// lane keeps real steering (parent authority changes the turn's course);
|
|
9
|
+
// an aside never claims that authority, and the envelope says so.
|
|
10
|
+
//
|
|
11
|
+
// Durability follows steer-queue's shape (chat_followups rows, restore
|
|
12
|
+
// only what never dispatched) with one aside-specific rule: the row id is
|
|
13
|
+
// the messageId dedupe key, it is written BEFORE the seam call, and the
|
|
14
|
+
// injected line lands on the transcript with queueId = row id BEFORE the
|
|
15
|
+
// row is deleted. A crash in either window is then self-healing: restore
|
|
16
|
+
// skips any row whose transcript already shows its marker (the words ran;
|
|
17
|
+
// never replay them) and requeues only the rest. "indeterminate" seam
|
|
18
|
+
// outcomes count as injected for the same reason — the words may already
|
|
19
|
+
// be running, and running them twice is the one mistake this lane must
|
|
20
|
+
// never make.
|
|
21
|
+
import { newId } from "./contracts.js";
|
|
22
|
+
import { chatFollowups, saveChatFollowup, settleChatFollowups } from "./message-db.js";
|
|
23
|
+
import { peerName } from "./peer-roster.js";
|
|
24
|
+
const asides = new Map(); // threadId → waiting asides
|
|
25
|
+
/** Threads with an injection or drain in flight: seam calls are awaited
|
|
26
|
+
* work, so two boundaries firing close together must not double-process
|
|
27
|
+
* the same words. */
|
|
28
|
+
const inFlight = new Set();
|
|
29
|
+
/** The mandatory non-steering envelope (audit §5.2). Every mid-turn channel
|
|
30
|
+
* OMB can ride is steering-branded, so the words themselves must say what
|
|
31
|
+
* they are: peer context from a named sender, not a course change. */
|
|
32
|
+
export function asideEnvelope(fromName, text) {
|
|
33
|
+
// Peer words sit between fixed markers, and a peer cannot be allowed to
|
|
34
|
+
// forge the closing marker inside its own text: that would end the aside
|
|
35
|
+
// early and make the batched context ambiguous downstream.
|
|
36
|
+
const body = text.replace(/\[\s*end\s+aside\s*\]/gi, "(end aside)");
|
|
37
|
+
return [
|
|
38
|
+
`[aside from @${peerName(fromName)} — peer context, not steering; continue your current plan unless this directly changes a fact you are using]`,
|
|
39
|
+
body,
|
|
40
|
+
"[end aside]",
|
|
41
|
+
].join("\n");
|
|
42
|
+
}
|
|
43
|
+
/** Rebuild the in-memory lane after a restart. Only rows whose dispatch
|
|
44
|
+
* never began come back; a row whose transcript already shows its queueId
|
|
45
|
+
* marker was injected before the process died, and those words must never
|
|
46
|
+
* run twice, so the row is retired instead of requeued. */
|
|
47
|
+
export function restoreAsideMessages(store) {
|
|
48
|
+
asides.clear();
|
|
49
|
+
for (const row of chatFollowups("aside")) {
|
|
50
|
+
if (row.status !== "pending")
|
|
51
|
+
continue;
|
|
52
|
+
if (store.messagesFor(row.threadId).some((message) => message.queueId === row.id)) {
|
|
53
|
+
settleChatFollowups([row.id], null);
|
|
54
|
+
continue;
|
|
55
|
+
}
|
|
56
|
+
const entry = asides.get(row.threadId) ?? { botId: row.ownerId, items: [] };
|
|
57
|
+
if (entry.botId !== row.ownerId)
|
|
58
|
+
throw new Error("queued aside belongs to another bot");
|
|
59
|
+
entry.items.push({
|
|
60
|
+
messageId: row.id,
|
|
61
|
+
text: row.payload.text,
|
|
62
|
+
prompt: row.payload.prompt ?? row.payload.text,
|
|
63
|
+
aside: row.payload.aside ?? { fromBotId: row.ownerId, fromBotName: "teammate", commsDepth: 0 },
|
|
64
|
+
trigger: row.payload.trigger,
|
|
65
|
+
});
|
|
66
|
+
asides.set(row.threadId, entry);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
/** Persist one aside and park it for injection. The row exists before any
|
|
70
|
+
* seam call so a crash can only ever re-queue words that never ran. */
|
|
71
|
+
export function queueAsideMessage(botId, threadId, text, options) {
|
|
72
|
+
const id = newId();
|
|
73
|
+
const item = {
|
|
74
|
+
messageId: id,
|
|
75
|
+
text,
|
|
76
|
+
prompt: asideEnvelope(options.fromBotName, text),
|
|
77
|
+
aside: {
|
|
78
|
+
fromBotId: options.fromBotId,
|
|
79
|
+
fromBotName: options.fromBotName,
|
|
80
|
+
...(options.unattended ? { unattended: true } : {}),
|
|
81
|
+
commsDepth: options.commsDepth,
|
|
82
|
+
},
|
|
83
|
+
trigger: options.trigger,
|
|
84
|
+
};
|
|
85
|
+
const entry = asides.get(threadId) ?? { botId, items: [] };
|
|
86
|
+
if (entry.botId !== botId)
|
|
87
|
+
throw new Error("queued aside belongs to another bot");
|
|
88
|
+
saveChatFollowup({
|
|
89
|
+
id,
|
|
90
|
+
kind: "aside",
|
|
91
|
+
ownerId: botId,
|
|
92
|
+
threadId,
|
|
93
|
+
payload: { text: item.text, prompt: item.prompt, aside: item.aside, ...(item.trigger ? { trigger: item.trigger } : {}) },
|
|
94
|
+
});
|
|
95
|
+
entry.items.push(item);
|
|
96
|
+
asides.set(threadId, entry);
|
|
97
|
+
return { id };
|
|
98
|
+
}
|
|
99
|
+
/** Everything waiting for this thread, in arrival order. */
|
|
100
|
+
export function pendingAsides(botId, threadId) {
|
|
101
|
+
const entry = asides.get(threadId);
|
|
102
|
+
return entry?.botId === botId ? [...entry.items] : [];
|
|
103
|
+
}
|
|
104
|
+
/** Record delivered asides: one transcript line per item (enveloped text,
|
|
105
|
+
* queueId marker, peer provenance), then the durable rows are deleted. The
|
|
106
|
+
* marker write precedes the delete, so the crash window between them heals
|
|
107
|
+
* as "already injected" on restore rather than as a replay. */
|
|
108
|
+
export function recordInjectedAsides(store, threadId, items) {
|
|
109
|
+
const appended = [];
|
|
110
|
+
for (const item of items) {
|
|
111
|
+
appended.push(store.appendMessage(threadId, {
|
|
112
|
+
role: "user",
|
|
113
|
+
kind: "text",
|
|
114
|
+
text: item.prompt,
|
|
115
|
+
queueId: item.messageId,
|
|
116
|
+
aside: true,
|
|
117
|
+
peerAsk: {
|
|
118
|
+
botId: item.aside.fromBotId,
|
|
119
|
+
name: item.aside.fromBotName,
|
|
120
|
+
...(item.aside.unattended ? { unattended: true } : {}),
|
|
121
|
+
},
|
|
122
|
+
}));
|
|
123
|
+
}
|
|
124
|
+
settleChatFollowups(items.map((item) => item.messageId), null);
|
|
125
|
+
// The words are delivered and their rows retired; the in-memory lane must
|
|
126
|
+
// agree, or a later boundary would batch them into another injection.
|
|
127
|
+
const entry = asides.get(threadId);
|
|
128
|
+
if (entry) {
|
|
129
|
+
const delivered = new Set(items.map((item) => item.messageId));
|
|
130
|
+
const remaining = entry.items.filter((item) => !delivered.has(item.messageId));
|
|
131
|
+
if (remaining.length)
|
|
132
|
+
asides.set(threadId, { botId: entry.botId, items: remaining });
|
|
133
|
+
else
|
|
134
|
+
asides.delete(threadId);
|
|
135
|
+
}
|
|
136
|
+
return appended;
|
|
137
|
+
}
|
|
138
|
+
/** Drop a thread's waiting asides: the bot or task is gone, so no turn will
|
|
139
|
+
* ever read them. Tombstoned like a cancelled follow-up. */
|
|
140
|
+
export function cancelAsides(threadId) {
|
|
141
|
+
const entry = asides.get(threadId);
|
|
142
|
+
if (!entry)
|
|
143
|
+
return;
|
|
144
|
+
settleChatFollowups(entry.items.map((item) => item.messageId), "cancelled");
|
|
145
|
+
asides.delete(threadId);
|
|
146
|
+
}
|
|
147
|
+
/** Fold everything waiting on this thread into its running turn.
|
|
148
|
+
*
|
|
149
|
+
* The rows are marked "dispatching" BEFORE the seam call: a crash at any
|
|
150
|
+
* point then leaves words the restart recovery path treats as may-have-run
|
|
151
|
+
* (recovered to the transcript with a review notice, never re-executed as
|
|
152
|
+
* a turn). "refused" — provably not delivered — returns them to "pending"
|
|
153
|
+
* so the next boundary can try again. "steered" and "indeterminate" both
|
|
154
|
+
* record delivery: the words may already be running, and running them
|
|
155
|
+
* twice is the one mistake this lane must never make.
|
|
156
|
+
*
|
|
157
|
+
* The batch leaves the shared queue BEFORE the seam call and stays out
|
|
158
|
+
* for the whole await: while the words may already be running inside the
|
|
159
|
+
* turn, no later boundary may be able to see them again, and a cancel
|
|
160
|
+
* landing mid-seam must not resurrect them. Only a "refused" outcome on a
|
|
161
|
+
* lane that is still ours puts the words back — ahead of anything that
|
|
162
|
+
* arrived during the call.
|
|
163
|
+
*
|
|
164
|
+
* Returns null when nothing is waiting or the thread is not this bot's;
|
|
165
|
+
* otherwise whether the batch was delivered and how many items it held. */
|
|
166
|
+
export async function attemptAsideInjection(store, botId, threadId, inject) {
|
|
167
|
+
const entry = asides.get(threadId);
|
|
168
|
+
if (!entry || entry.botId !== botId || entry.items.length === 0)
|
|
169
|
+
return null;
|
|
170
|
+
if (inFlight.has(threadId))
|
|
171
|
+
return { delivered: false, count: entry.items.length };
|
|
172
|
+
const batch = entry.items;
|
|
173
|
+
entry.items = [];
|
|
174
|
+
const ids = batch.map((item) => item.messageId);
|
|
175
|
+
settleChatFollowups(ids, "dispatching");
|
|
176
|
+
inFlight.add(threadId);
|
|
177
|
+
let outcome;
|
|
178
|
+
try {
|
|
179
|
+
outcome = await inject(botId, threadId, batch.map((item) => item.prompt).join("\n\n"));
|
|
180
|
+
}
|
|
181
|
+
catch {
|
|
182
|
+
outcome = "indeterminate";
|
|
183
|
+
}
|
|
184
|
+
finally {
|
|
185
|
+
inFlight.delete(threadId);
|
|
186
|
+
}
|
|
187
|
+
if (outcome === "refused") {
|
|
188
|
+
if (asides.get(threadId) === entry) {
|
|
189
|
+
// Still our lane: back on the queue, ahead of anything that
|
|
190
|
+
// arrived during the seam call.
|
|
191
|
+
entry.items = [...batch, ...entry.items];
|
|
192
|
+
settleChatFollowups(ids, "pending");
|
|
193
|
+
}
|
|
194
|
+
else {
|
|
195
|
+
// cancelAsides took the lane while the seam call was awaited: the
|
|
196
|
+
// words never ran, but their lane is gone, so they retire with it
|
|
197
|
+
// instead of coming back as phantom rows after a restart.
|
|
198
|
+
settleChatFollowups(ids, "cancelled");
|
|
199
|
+
}
|
|
200
|
+
return { delivered: false, count: batch.length };
|
|
201
|
+
}
|
|
202
|
+
try {
|
|
203
|
+
recordInjectedAsides(store, threadId, batch);
|
|
204
|
+
}
|
|
205
|
+
catch (error) {
|
|
206
|
+
// The seam says the words may already be running; a failed transcript
|
|
207
|
+
// write must never hand them to a later boundary. Retire the rows
|
|
208
|
+
// loudly instead of replaying them.
|
|
209
|
+
console.error("aside-queue: could not record injected asides", error);
|
|
210
|
+
settleChatFollowups(ids, null);
|
|
211
|
+
if (asides.get(threadId) === entry && entry.items.length === 0)
|
|
212
|
+
asides.delete(threadId);
|
|
213
|
+
}
|
|
214
|
+
return { delivered: true, count: batch.length };
|
|
215
|
+
}
|
|
216
|
+
/**
|
|
217
|
+
* One boundary pass over the aside lane.
|
|
218
|
+
*
|
|
219
|
+
* Injection first: a thread already running a seam-capable turn gets every
|
|
220
|
+
* waiting aside folded in as ONE steer call (envelopes joined by a blank
|
|
221
|
+
* line — one boundary, one batch). Otherwise, when the thread is idle and
|
|
222
|
+
* unblocked, the asides degrade to a single enveloped follow-up turn, the
|
|
223
|
+
* same shape the steer-queue gives queued person messages. Callers run the
|
|
224
|
+
* steer drain before this one, so at a shared boundary the person's queued
|
|
225
|
+
* correction lands ahead of peer context.
|
|
226
|
+
*/
|
|
227
|
+
export async function drainAsideMessages(options) {
|
|
228
|
+
const { store, inject, run, isBlocked } = options;
|
|
229
|
+
for (const [threadId, entry] of asides) {
|
|
230
|
+
if (inFlight.has(threadId))
|
|
231
|
+
continue;
|
|
232
|
+
const bot = store.projectBotForTask
|
|
233
|
+
? store.projectBotForTask(entry.botId, threadId)
|
|
234
|
+
: store.bot(entry.botId);
|
|
235
|
+
if (!bot) {
|
|
236
|
+
// the bot or task was deleted while the aside waited
|
|
237
|
+
cancelAsides(threadId);
|
|
238
|
+
continue;
|
|
239
|
+
}
|
|
240
|
+
if (bot.busy) {
|
|
241
|
+
// Seam attempt: one batched call for everything waiting here.
|
|
242
|
+
await attemptAsideInjection(store, entry.botId, threadId, inject);
|
|
243
|
+
continue;
|
|
244
|
+
}
|
|
245
|
+
if (isBlocked?.(entry.botId, threadId))
|
|
246
|
+
continue;
|
|
247
|
+
// Degrade to one follow-up turn. The entry leaves the map before
|
|
248
|
+
// anything runs, so a settle racing another settle can never fire the
|
|
249
|
+
// same asides twice.
|
|
250
|
+
const items = entry.items;
|
|
251
|
+
const ids = items.map((item) => item.messageId);
|
|
252
|
+
settleChatFollowups(ids, "dispatching");
|
|
253
|
+
asides.delete(threadId);
|
|
254
|
+
const appended = recordDegradedAsides(store, threadId, items);
|
|
255
|
+
const last = appended.at(-1);
|
|
256
|
+
if (!last) {
|
|
257
|
+
settleChatFollowups(ids, null);
|
|
258
|
+
continue;
|
|
259
|
+
}
|
|
260
|
+
const running = run(entry.botId, threadId, items.map((item) => item.prompt).join("\n\n"), last, appended.map((message) => message.id), { trigger: items[0].trigger, aside: items[0].aside });
|
|
261
|
+
void Promise.resolve(running).then(() => settleChatFollowups(ids, null), () => settleChatFollowups(ids, "interrupted")).catch((error) => console.warn("aside-queue: could not settle degraded aside", error));
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
/** Append the waiting asides as transcript lines for the degraded turn.
|
|
265
|
+
* Same record shape as injection — enveloped text, queueId marker, peer
|
|
266
|
+
* provenance — so both deliveries read identically downstream. */
|
|
267
|
+
function recordDegradedAsides(store, threadId, items) {
|
|
268
|
+
const appended = [];
|
|
269
|
+
for (const item of items) {
|
|
270
|
+
appended.push(store.appendMessage(threadId, {
|
|
271
|
+
role: "user",
|
|
272
|
+
kind: "text",
|
|
273
|
+
text: item.prompt,
|
|
274
|
+
queueId: item.messageId,
|
|
275
|
+
aside: true,
|
|
276
|
+
peerAsk: {
|
|
277
|
+
botId: item.aside.fromBotId,
|
|
278
|
+
name: item.aside.fromBotName,
|
|
279
|
+
...(item.aside.unattended ? { unattended: true } : {}),
|
|
280
|
+
},
|
|
281
|
+
}));
|
|
282
|
+
}
|
|
283
|
+
return appended;
|
|
284
|
+
}
|
|
285
|
+
/** Whether a thread currently has an injection or drain in flight. The
|
|
286
|
+
* arrival path uses this to stay honest ("queued") instead of racing a
|
|
287
|
+
* boundary pass that may be folding the same lane right now. */
|
|
288
|
+
export function asideAttemptInFlight(threadId) {
|
|
289
|
+
return inFlight.has(threadId);
|
|
290
|
+
}
|
|
291
|
+
/** Test helper: how many asides remain queued for a thread. */
|
|
292
|
+
export function _asideCount(threadId) {
|
|
293
|
+
return asides.get(threadId)?.items.length ?? 0;
|
|
294
|
+
}
|
|
@@ -61,6 +61,9 @@ const IMAGE_MIMES = {
|
|
|
61
61
|
* despite using ZIP internally. The claimed mime determines the extension;
|
|
62
62
|
* an attacker-controlled filename never does. */
|
|
63
63
|
const FILE_MIMES = {
|
|
64
|
+
"video/mp4": ".mp4",
|
|
65
|
+
"video/webm": ".webm",
|
|
66
|
+
"video/quicktime": ".mov",
|
|
64
67
|
"audio/opus": ".opus",
|
|
65
68
|
"audio/ogg": ".ogg",
|
|
66
69
|
"audio/mpeg": ".mp3",
|
|
@@ -539,6 +542,61 @@ export function saveImage(bytes, mime, requestedUploadId) {
|
|
|
539
542
|
reservation.release();
|
|
540
543
|
}
|
|
541
544
|
}
|
|
545
|
+
/** Persist one synthesized audio note. Providers return audio/mpeg buffers;
|
|
546
|
+
* the same atomic-write and quota discipline as saveImage applies, with a
|
|
547
|
+
* fixed .mp3 extension because the accepted mime determines it exactly. */
|
|
548
|
+
export function saveAudio(bytes, mime) {
|
|
549
|
+
const normalized = mime.split(";")[0].trim().toLowerCase();
|
|
550
|
+
if (normalized !== "audio/mpeg") {
|
|
551
|
+
throw Object.assign(new Error("unsupported audio type"), { status: 400 });
|
|
552
|
+
}
|
|
553
|
+
if (bytes.byteLength === 0)
|
|
554
|
+
throw Object.assign(new Error("empty audio"), { status: 400 });
|
|
555
|
+
if (bytes.byteLength > FILE_MAX_BYTES) {
|
|
556
|
+
throw Object.assign(new Error(`audio exceeds ${FILE_MAX_BYTES} bytes`), { status: 413 });
|
|
557
|
+
}
|
|
558
|
+
ensureAttachmentsDir();
|
|
559
|
+
const reservation = new AttachmentReservation();
|
|
560
|
+
reservation.reserve(bytes.byteLength);
|
|
561
|
+
const id = randomUUID();
|
|
562
|
+
const name = `${id}.mp3`;
|
|
563
|
+
const path = join(ATTACHMENTS_DIR, name);
|
|
564
|
+
const partialPath = join(ATTACHMENTS_DIR, `.openmaus-upload-${id}-${randomUUID()}.partial`);
|
|
565
|
+
activePartials.add(partialPath);
|
|
566
|
+
let partialCleanupFailed = false;
|
|
567
|
+
try {
|
|
568
|
+
writeFileSync(partialPath, bytes, { mode: 0o600, flag: "wx" });
|
|
569
|
+
try {
|
|
570
|
+
linkSync(partialPath, path);
|
|
571
|
+
addCommittedBytes(bytes.byteLength);
|
|
572
|
+
}
|
|
573
|
+
catch (error) {
|
|
574
|
+
if (error.code !== "EEXIST")
|
|
575
|
+
throw error;
|
|
576
|
+
const saved = readFileSync(path);
|
|
577
|
+
if (!saved.equals(bytes)) {
|
|
578
|
+
throw Object.assign(new Error("audio destination already exists with different bytes"), { status: 409 });
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
unlinkSync(partialPath);
|
|
582
|
+
return { path, mime: normalized, bytes: bytes.byteLength };
|
|
583
|
+
}
|
|
584
|
+
catch (error) {
|
|
585
|
+
try {
|
|
586
|
+
unlinkSync(partialPath);
|
|
587
|
+
}
|
|
588
|
+
catch {
|
|
589
|
+
partialCleanupFailed = true;
|
|
590
|
+
}
|
|
591
|
+
throw error;
|
|
592
|
+
}
|
|
593
|
+
finally {
|
|
594
|
+
activePartials.delete(partialPath);
|
|
595
|
+
if (partialCleanupFailed)
|
|
596
|
+
invalidateAttachmentAccounting();
|
|
597
|
+
reservation.release();
|
|
598
|
+
}
|
|
599
|
+
}
|
|
542
600
|
/** HTTP uploads take the same per-ID lock as streamed files. saveImage stays
|
|
543
601
|
* synchronous for generated avatars, while this wrapper prevents an image
|
|
544
602
|
* and a document using the same caller-supplied UUID from committing with
|
|
@@ -567,7 +625,7 @@ export function attachmentExists(name) {
|
|
|
567
625
|
* filename (no separators, no dotfiles) inside ATTACHMENTS_DIR resolve —
|
|
568
626
|
* the route must never become a general file server for the data dir. */
|
|
569
627
|
export function readAttachment(name) {
|
|
570
|
-
if (!/^[A-Za-z0-9-]+\.(png|jpg|jpeg|gif|webp)$/.test(name))
|
|
628
|
+
if (!/^[A-Za-z0-9-]+\.(png|jpg|jpeg|gif|webp|mp3)$/.test(name))
|
|
571
629
|
return null;
|
|
572
630
|
const path = join(ATTACHMENTS_DIR, name);
|
|
573
631
|
if (extname(path) === ".jpeg")
|
|
@@ -589,7 +647,46 @@ function mimeForExt(ext) {
|
|
|
589
647
|
return "image/gif";
|
|
590
648
|
case ".webp":
|
|
591
649
|
return "image/webp";
|
|
650
|
+
case ".mp3":
|
|
651
|
+
return "audio/mpeg";
|
|
592
652
|
default:
|
|
593
653
|
return "application/octet-stream";
|
|
594
654
|
}
|
|
595
655
|
}
|
|
656
|
+
/** Parse a single `bytes=` range against a stored audio file. Only the
|
|
657
|
+
* canonical shapes a player sends are honored: `a-b`, `a-`, and `-n`
|
|
658
|
+
* (the last-n-bytes suffix). Anything else — commas, letters, reversed
|
|
659
|
+
* bounds, internal spaces — reads as "not understood" and the caller sends
|
|
660
|
+
* the whole file, which is always a correct answer to a GET. */
|
|
661
|
+
export function parseAudioRange(header, size) {
|
|
662
|
+
const raw = header?.trim();
|
|
663
|
+
if (!raw)
|
|
664
|
+
return { kind: "none" };
|
|
665
|
+
const m = raw.match(/^bytes=(\d*)-(\d*)$/);
|
|
666
|
+
if (!m)
|
|
667
|
+
return { kind: "none" };
|
|
668
|
+
const [, rawStart, rawEnd] = m;
|
|
669
|
+
if (rawStart === "") {
|
|
670
|
+
// `bytes=-` carries no bounds at all: malformed input, not a suffix
|
|
671
|
+
// range, so the caller answers with the whole file. A real zero-length
|
|
672
|
+
// suffix (`bytes=-0`) stays unsatisfiable below, per RFC 9110.
|
|
673
|
+
if (rawEnd === "")
|
|
674
|
+
return { kind: "none" };
|
|
675
|
+
// suffix range: the final n bytes; zero is unsatisfiable by definition
|
|
676
|
+
const suffix = Number(rawEnd);
|
|
677
|
+
if (!suffix)
|
|
678
|
+
return { kind: "unsatisfiable" };
|
|
679
|
+
if (size === 0)
|
|
680
|
+
return { kind: "unsatisfiable" };
|
|
681
|
+
return { kind: "range", start: Math.max(0, size - suffix), end: size - 1 };
|
|
682
|
+
}
|
|
683
|
+
const start = Number(rawStart);
|
|
684
|
+
if (start >= size)
|
|
685
|
+
return { kind: "unsatisfiable" };
|
|
686
|
+
if (rawEnd === "")
|
|
687
|
+
return { kind: "range", start, end: size - 1 };
|
|
688
|
+
const end = Number(rawEnd);
|
|
689
|
+
if (end < start)
|
|
690
|
+
return { kind: "none" };
|
|
691
|
+
return { kind: "range", start, end: Math.min(end, size - 1) };
|
|
692
|
+
}
|
|
@@ -4,8 +4,7 @@
|
|
|
4
4
|
// provider's own permission mode passed straight through (Claude `auto`,
|
|
5
5
|
// Grok `--permission-mode`, Codex `approvalsReviewer`, …), and a request that
|
|
6
6
|
// reaches this process is one the provider left for a person. The only
|
|
7
|
-
//
|
|
8
|
-
// person's explicit, separately confirmed grant to answer every prompt.
|
|
7
|
+
// grants the app applies are Full access and the person's exact saved commands.
|
|
9
8
|
// Questions never come through here: a bot's question always reaches a human.
|
|
10
9
|
import { supportsApprovalMode } from "../shared/approval-mode.js";
|
|
11
10
|
/** A failed delivery is a runtime error, not another permission decision.
|
|
@@ -55,7 +54,7 @@ export function delegationInheritsFullAccess(input) {
|
|
|
55
54
|
// the permission path in permission-proxy). Approving it there does not
|
|
56
55
|
// produce an answer: the CLI runs the tool with none and the model is told
|
|
57
56
|
// "The user did not answer the questions." — a question silently lost.
|
|
58
|
-
const ASKS_A_PERSON = new Set(["askuserquestion", "ask_user"]);
|
|
57
|
+
const ASKS_A_PERSON = new Set(["askuserquestion", "ask_user", "omb-ask"]);
|
|
59
58
|
export function autoVerdict(mode, tool, context) {
|
|
60
59
|
// A question is for a person, whatever channel it arrived on — and
|
|
61
60
|
// whatever the mode: even Full has no answer to give, only an approval
|
|
@@ -70,6 +69,8 @@ export function autoVerdict(mode, tool, context) {
|
|
|
70
69
|
return { approve: `approved ${tool} (full access)`, source: "full-access" };
|
|
71
70
|
if (context?.requiresExplicitApproval)
|
|
72
71
|
return { approve: null, source: "explicit-approval-block" };
|
|
72
|
+
if (context?.commandAllowed)
|
|
73
|
+
return { approve: `approved ${tool} (saved command)`, source: "command-allowlist" };
|
|
73
74
|
if (mode === "auto" || mode === "custom")
|
|
74
75
|
return { approve: null, source: "native-approval" };
|
|
75
76
|
return { approve: null, source: "no-grant" };
|
|
@@ -18,5 +18,6 @@ export function startAutoVmClaim(table, threadId, generation) {
|
|
|
18
18
|
slot.begin = slot.claim().then(() => { slot.claimed = true; }, (error) => {
|
|
19
19
|
slot.failed = true;
|
|
20
20
|
slot.failure = error instanceof Error ? error.message : String(error);
|
|
21
|
+
slot.onRejected?.(slot.failure);
|
|
21
22
|
});
|
|
22
23
|
}
|