openmausbot 0.1.95 → 0.1.97
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/{DesktopViewer-BwWpc4RK.js → DesktopViewer-CPidMCtt.js} +1 -1
- package/dist/assets/{abnfDiagram-O67JEVCF-jsQkIqTn.js → abnfDiagram-O67JEVCF-k1UCNXb7.js} +1 -1
- package/dist/assets/{arc-7kei4Hsu.js → arc-BUWGa9UU.js} +1 -1
- package/dist/assets/{architectureDiagram-NJMV4G6O-CRWDFDMq.js → architectureDiagram-NJMV4G6O-DiPor3UT.js} +1 -1
- package/dist/assets/{blockDiagram-BEXU5L5S-CTKW_IKs.js → blockDiagram-BEXU5L5S-Ag9nLW4H.js} +1 -1
- package/dist/assets/{c4Diagram-YGBWAQC7-BBTW7-6M.js → c4Diagram-YGBWAQC7-Bh0sjgUa.js} +1 -1
- package/dist/assets/channel-DR8eD6QW.js +1 -0
- package/dist/assets/{chunk-2Q5K7J3B-M_Ox-MkD.js → chunk-2Q5K7J3B-BFBcIIqu.js} +1 -1
- package/dist/assets/{chunk-3FUC2YCW-D2YiN-Ud.js → chunk-3FUC2YCW-Dvfpex-f.js} +2 -2
- package/dist/assets/{chunk-5VM5RSS4-BmVYJ0f8.js → chunk-5VM5RSS4-CMjP2YZW.js} +1 -1
- package/dist/assets/{chunk-GTNCS2PH-BtiLVVnZ.js → chunk-GTNCS2PH-BryoeTc7.js} +1 -1
- package/dist/assets/{chunk-GWA4HPMP-DFYSb6J6.js → chunk-GWA4HPMP-BhfvcchO.js} +1 -1
- package/dist/assets/{chunk-JWPE2WC7-mknKBzgV.js → chunk-JWPE2WC7-bcCVp8rp.js} +1 -1
- package/dist/assets/{chunk-NETBCI7D-DgSYGiOu.js → chunk-NETBCI7D-yKIjIlkQ.js} +1 -1
- package/dist/assets/{chunk-WEXAMYUT-C7ZvuguH.js → chunk-WEXAMYUT-D3Zqbmup.js} +1 -1
- package/dist/assets/{chunk-XXDRQBXY-CPqxINpA.js → chunk-XXDRQBXY-CjVoIMG6.js} +1 -1
- package/dist/assets/{classDiagram-v2-NBCMYWYE-Cd6xfJaC.js → classDiagram-v2-NBCMYWYE-mUCWdVyH.js} +1 -1
- package/dist/assets/{cose-bilkent-JH36ORCC-BSXY5qRh.js → cose-bilkent-JH36ORCC-BBmR1bbX.js} +1 -1
- package/dist/assets/{cynefin-OW5HDTMX-D_lhEMj1.js → cynefin-OW5HDTMX-BpRrxoO4.js} +1 -1
- package/dist/assets/{cynefinDiagram-VND7K2PF-Da9FrFwk.js → cynefinDiagram-VND7K2PF-D3VBWCPK.js} +1 -1
- package/dist/assets/{dagre-6A5THRUB-T77jMOXe.js → dagre-6A5THRUB-DxEIACOO.js} +1 -1
- package/dist/assets/{diagram-22UHCM2B-Dkj96X5E.js → diagram-22UHCM2B-BANgZEOS.js} +1 -1
- package/dist/assets/{diagram-3UASUU5V-CMi9zlD_.js → diagram-3UASUU5V-Cnq7nJir.js} +1 -1
- package/dist/assets/{diagram-ATOU4E4O-CXK6YUfS.js → diagram-ATOU4E4O-4TBDIWnz.js} +1 -1
- package/dist/assets/{diagram-CDSNMT55-BE7B1u9Q.js → diagram-CDSNMT55-CD_B39D4.js} +1 -1
- package/dist/assets/{diagram-MLGK6HIB-CCMQphKr.js → diagram-MLGK6HIB-CzvnFhEC.js} +1 -1
- package/dist/assets/{diagram-MPIPVDR6-iHJ_-tGH.js → diagram-MPIPVDR6-C4w2SNml.js} +1 -1
- package/dist/assets/{ebnfDiagram-ZINNZB2B-DONsCWQ7.js → ebnfDiagram-ZINNZB2B-gkUEBm2s.js} +1 -1
- package/dist/assets/{elk-276RUBZZ-CU_WOaXk.js → elk-276RUBZZ-B_56X6mw.js} +1 -1
- package/dist/assets/{erDiagram-OPXOYQCR-BeYwM04G.js → erDiagram-OPXOYQCR-C_oByoDS.js} +1 -1
- package/dist/assets/{flowDiagram-KWPJA3E3-Bdw9DqEl.js → flowDiagram-KWPJA3E3-CR9xXJkC.js} +1 -1
- package/dist/assets/{ganttDiagram-FUAMR5RP-C9iwq3qm.js → ganttDiagram-FUAMR5RP-gtYBTu0B.js} +1 -1
- package/dist/assets/{gitGraphDiagram-X574FWY7-Dx-VWzUg.js → gitGraphDiagram-X574FWY7-VJ6-Tp_p.js} +1 -1
- package/dist/assets/{index-KygHRE3E.js → index-Cp4S4NOL.js} +1 -1
- package/dist/assets/index-CpaIXRLp.js +614 -0
- package/dist/assets/index-Z81SMsgR.css +1 -0
- package/dist/assets/{infoDiagram-VRGFBTTK-BG7uaEd4.js → infoDiagram-VRGFBTTK-2Lu9te4D.js} +1 -1
- package/dist/assets/{ishikawaDiagram-OU5B5YK6-D72WAXnM.js → ishikawaDiagram-OU5B5YK6-DR49XY-e.js} +1 -1
- package/dist/assets/{journeyDiagram-ZHPQQLJL-DKH1gjYT.js → journeyDiagram-ZHPQQLJL-ZoR0M305.js} +1 -1
- package/dist/assets/{kanban-definition-PNTS6WVX-3upm9JUT.js → kanban-definition-PNTS6WVX-DLKHxrO-.js} +1 -1
- package/dist/assets/{linear-CcR4pc0-.js → linear-BASsfWly.js} +1 -1
- package/dist/assets/{mermaid.core-C6yU5FSS.js → mermaid.core-BVidiPkk.js} +5 -5
- package/dist/assets/{mindmap-definition-NLK3R4M7-DOI2xEuv.js → mindmap-definition-NLK3R4M7-g_Qx0U5C.js} +1 -1
- package/dist/assets/{pegDiagram-GJSIUBJH-BVWJSgLd.js → pegDiagram-GJSIUBJH-DI9Cjs8w.js} +1 -1
- package/dist/assets/{pieDiagram-5QR66LMP-jtbYezhq.js → pieDiagram-5QR66LMP-sRI0Uhr5.js} +1 -1
- package/dist/assets/{quadrantDiagram-O4NWA36T-g0u8mtPX.js → quadrantDiagram-O4NWA36T-CamKTby3.js} +1 -1
- package/dist/assets/{railroadDiagram-XR7U4H2S-k8689Wny.js → railroadDiagram-XR7U4H2S-DOLlkbf3.js} +1 -1
- package/dist/assets/{requirementDiagram-PLB6GJNP-DsJ22PDt.js → requirementDiagram-PLB6GJNP-DrRQ5r97.js} +1 -1
- package/dist/assets/{sankeyDiagram-IPEJSGJF-CneWBRgG.js → sankeyDiagram-IPEJSGJF-DGS_lXcX.js} +1 -1
- package/dist/assets/{sequenceDiagram-PO4LG4MO-DWsFCWap.js → sequenceDiagram-PO4LG4MO-CkjWqwtf.js} +1 -1
- package/dist/assets/{sizeCapture-INFHLROL-Bvf0YE3S.js → sizeCapture-INFHLROL-CepRgful.js} +1 -1
- package/dist/assets/{stateDiagram-v2-GCMORJYK-Bxb3Iei8.js → stateDiagram-v2-GCMORJYK-hykr-9RS.js} +1 -1
- package/dist/assets/{swimlanes-2SLR337P-DLkk7W4A.js → swimlanes-2SLR337P-CKOHEZbI.js} +1 -1
- package/dist/assets/swimlanesDiagram-TC7HE7FX-D-8vqZ4-.js +8 -0
- package/dist/assets/{timeline-definition-EJHVYXUP-KR9bIHYC.js → timeline-definition-EJHVYXUP-CB8pJ6AC.js} +1 -1
- package/dist/assets/{usecaseDiagram-POWQR4AR-DfsOrj8t.js → usecaseDiagram-POWQR4AR-DrclJHdl.js} +1 -1
- package/dist/assets/{vennDiagram-UO4OBE2U-DRSTzG3h.js → vennDiagram-UO4OBE2U-zS23qjv8.js} +1 -1
- package/dist/assets/{wardleyDiagram-VNRHLVJA-DtEdhPgT.js → wardleyDiagram-VNRHLVJA-52jgeKxz.js} +1 -1
- package/dist/assets/{xychartDiagram-PMCCYNJV-B-XhTxjG.js → xychartDiagram-PMCCYNJV-DSdl9rQ8.js} +1 -1
- package/dist/index.html +2 -2
- package/dist-server/cloud-home-start.js +26 -13
- package/dist-server/companion/src/listener.js +39 -18
- package/dist-server/container-mcp.js +14 -27
- package/dist-server/drivers/agents-proxy.js +108 -49
- package/dist-server/harness-mcp-proxy.js +1 -2
- package/dist-server/index.js +4664 -4644
- package/dist-server/openmausbot.js +1425 -1437
- package/dist-server/pair-cli.js +1425 -1437
- package/dist-server/permission-proxy.js +0 -4
- package/dist-server/server/admin-activity.js +1 -4
- package/dist-server/server/auto-approve.js +20 -5
- package/dist-server/server/auto-vm-claims.js +13 -1
- package/dist-server/server/automatic-recovery.js +1 -2
- package/dist-server/server/boat.js +71 -87
- package/dist-server/server/bot-overview.js +1 -1
- package/dist-server/server/browser-engine.js +38 -1
- package/dist-server/server/browser-live.js +112 -25
- package/dist-server/server/browser-runtime.js +31 -1
- package/dist-server/server/checkpoints.js +340 -167
- package/dist-server/server/chief-of-staff.js +7 -6
- package/dist-server/server/cli-setup.js +3 -3
- package/dist-server/server/cli.js +33 -26
- package/dist-server/server/cloud-computer-tools.js +11 -4
- package/dist-server/server/cloud-home-start.js +13 -16
- package/dist-server/server/cloud-home.js +3 -13
- package/dist-server/server/cloud-move-http.js +44 -30
- package/dist-server/server/cloud-move.js +20 -18
- package/dist-server/server/cloud-owner.js +1 -1
- package/dist-server/server/composio.js +10 -3
- package/dist-server/server/computer-engine-removal.js +70 -0
- package/dist-server/server/computer-selection.js +18 -0
- package/dist-server/server/config.js +99 -75
- package/dist-server/server/connector-scope-hint.js +83 -0
- package/dist-server/server/container-computer.js +16 -5
- package/dist-server/server/contracts.js +0 -9
- package/dist-server/server/decider/jev.js +1 -4
- package/dist-server/server/decider/log.js +1 -3
- package/dist-server/server/decision-log.js +1 -4
- package/dist-server/server/default-model-selection.js +20 -14
- package/dist-server/server/delegations.js +14 -7
- package/dist-server/server/delta-context.js +6 -1
- package/dist-server/server/drivers/acp/core.js +5 -4
- package/dist-server/server/drivers/acp/opencode-go.js +19 -23
- package/dist-server/server/drivers/agents-call.js +47 -64
- package/dist-server/server/drivers/agents-catalog.js +69 -3
- package/dist-server/server/drivers/agents-proxy.js +2 -1
- package/dist-server/server/drivers/builtIn.js +0 -2
- package/dist-server/server/drivers/claude-login-auth.js +2 -11
- package/dist-server/server/drivers/claude.js +49 -30
- package/dist-server/server/drivers/codex-device-auth.js +3 -1
- package/dist-server/server/drivers/codex.js +7 -6
- package/dist-server/server/drivers/grok.js +1 -1
- package/dist-server/server/drivers/openai-chat-protocol.js +4 -2
- package/dist-server/server/drivers/openai-chat.js +113 -11
- package/dist-server/server/drivers/pi.js +184 -49
- package/dist-server/server/drivers/prompt-split.js +4 -4
- package/dist-server/server/harness/bus.js +49 -0
- package/dist-server/server/harness-mcp-proxy.js +1 -1
- package/dist-server/server/index.js +942 -980
- package/dist-server/server/live-call.js +11 -6
- package/dist-server/server/managed-policy.js +1 -1
- package/dist-server/server/mcp-registry.js +35 -0
- package/dist-server/server/message-file.js +4 -4
- package/dist-server/server/model-catalog/presets.js +8 -0
- package/dist-server/server/peer-delivery.js +4 -19
- package/dist-server/server/peer-roster.js +40 -6
- package/dist-server/server/profile-versions.js +0 -31
- package/dist-server/server/provider-key-check.js +2 -1
- package/dist-server/server/restart.js +40 -0
- package/dist-server/server/room-handoffs.js +12 -0
- package/dist-server/server/room-turn-end.js +15 -0
- package/dist-server/server/routes/bot-memory.js +0 -24
- package/dist-server/server/routes/live.js +3 -3
- package/dist-server/server/routine-requests.js +2 -5
- package/dist-server/server/routines.js +6 -5
- package/dist-server/server/screen-frame-gate.js +1 -3
- package/dist-server/server/server-launcher.js +53 -0
- package/dist-server/server/store.js +126 -2
- package/dist-server/server/surface.js +40 -64
- package/dist-server/server/system-prompt.js +23 -13
- package/dist-server/server/team-manifest.js +1 -6
- package/dist-server/server/tunnel.js +4 -1
- package/dist-server/server/turn-continuation.js +162 -0
- package/dist-server/server/turn-images.js +1 -21
- package/dist-server/server/turn-resources.js +19 -150
- package/dist-server/server/usage-ledger.js +3 -1
- package/dist-server/server/workspace.js +3 -4
- package/dist-server/server-launcher.js +72 -0
- package/dist-server/shared/ask-question.js +1 -71
- package/dist-server/shared/cloud-computer.js +10 -0
- package/dist-server/shared/credential-request.js +1 -1
- package/dist-server/shared/failed-turn.js +22 -0
- package/dist-server/shared/live-approval.js +2 -2
- package/dist-server/shared/package-format.js +3 -3
- package/dist-server/shared/pairing-link.js +144 -0
- package/dist-server/shared/place-view.js +409 -0
- package/dist-server/shared/tool-scope-support.js +1 -2
- package/dist-server/vps-container-mcp.js +14 -27
- package/dist-server/workspace-backup.worker.js +14 -27
- package/package.json +1 -1
- package/dist/assets/channel-DS2NbT5x.js +0 -1
- package/dist/assets/index-NF7XCHAy.css +0 -1
- package/dist/assets/index-X_5ydXIE.js +0 -610
- package/dist/assets/swimlanesDiagram-TC7HE7FX-C6bZtBXk.js +0 -8
- package/dist-server/server/claim-idle.js +0 -33
- package/dist-server/server/cloud-overflow.js +0 -167
- package/dist-server/server/drivers/boatagent.js +0 -443
- package/dist-server/server/tightening-requests.js +0 -435
- package/dist-server/shared/tightening-request.js +0 -87
|
@@ -9,6 +9,7 @@ import { lstatSync, realpathSync, statSync } from "node:fs";
|
|
|
9
9
|
import { dirname, extname, resolve } from "node:path";
|
|
10
10
|
import { fromMarkdown } from "mdast-util-from-markdown";
|
|
11
11
|
import { ATTACHMENTS_DIR, IMAGE_MAX_BYTES } from "./attachments.js";
|
|
12
|
+
import { decodeAttachmentAttribute } from "./message-file.js";
|
|
12
13
|
/** Matches the companion composer policy. Four maximum-sized images are
|
|
13
14
|
* bounded to 40 MiB before a provider is asked to ingest them. */
|
|
14
15
|
export const TURN_IMAGE_MAX_COUNT = 4;
|
|
@@ -24,27 +25,6 @@ const MIME_BY_EXTENSION = {
|
|
|
24
25
|
function statusError(status, message) {
|
|
25
26
|
return Object.assign(new Error(message), { status });
|
|
26
27
|
}
|
|
27
|
-
/** Decode exactly the entities emitted by the composer. Repeated or unknown
|
|
28
|
-
* encodings stay encoded and therefore cannot acquire a privileged path. */
|
|
29
|
-
function decodeAttachmentAttribute(value) {
|
|
30
|
-
return value.replace(/&(quot|lt|gt|amp);|&#(9|10|13);/g, (entity, named, numeric) => {
|
|
31
|
-
if (numeric === "9")
|
|
32
|
-
return "\t";
|
|
33
|
-
if (numeric === "10")
|
|
34
|
-
return "\n";
|
|
35
|
-
if (numeric === "13")
|
|
36
|
-
return "\r";
|
|
37
|
-
if (named === "quot")
|
|
38
|
-
return '"';
|
|
39
|
-
if (named === "lt")
|
|
40
|
-
return "<";
|
|
41
|
-
if (named === "gt")
|
|
42
|
-
return ">";
|
|
43
|
-
if (named === "amp")
|
|
44
|
-
return "&";
|
|
45
|
-
return entity;
|
|
46
|
-
});
|
|
47
|
-
}
|
|
48
28
|
function walk(node, visit) {
|
|
49
29
|
const pending = [node];
|
|
50
30
|
while (pending.length > 0) {
|
|
@@ -1,12 +1,12 @@
|
|
|
1
|
-
import { realpathSync } from "node:fs";
|
|
2
|
-
import { relative, resolve, sep } from "node:path";
|
|
3
1
|
/** One harness owns the data directory. Claims are synchronous and last for
|
|
4
2
|
* the whole turn, not just a click: a screenshot and its following click
|
|
5
|
-
* must see the same desktop. These coordinate app-managed resources
|
|
6
|
-
*
|
|
3
|
+
* must see the same desktop. These coordinate app-managed resources — a
|
|
4
|
+
* desktop, a browser session, a phone — that only one turn can drive at a
|
|
5
|
+
* time. A project folder is not one: a bot's threads work in one folder
|
|
6
|
+
* side by side, as several agent sessions do in one repo. They are not a
|
|
7
|
+
* sandbox for arbitrary shell commands. */
|
|
7
8
|
export class TurnResources {
|
|
8
9
|
owners = new Map();
|
|
9
|
-
reclaims = new Map();
|
|
10
10
|
/** Arrival-ordered waiters per requested resource (#1652). Entries exist
|
|
11
11
|
* only while a turn is genuinely waiting (the exclusive bind's poll loop):
|
|
12
12
|
* a lazy claim that failed once and moved on must not sit at the front and
|
|
@@ -72,25 +72,16 @@ export class TurnResources {
|
|
|
72
72
|
waitEstimateMs(resource) {
|
|
73
73
|
return this.waitStats.get(resource)?.ewmaMs;
|
|
74
74
|
}
|
|
75
|
-
blocker(resource, owner
|
|
76
|
-
this.
|
|
77
|
-
|
|
78
|
-
if (overlaps(key, resource) && !sameOwner(current.owner, owner))
|
|
79
|
-
return current.owner;
|
|
80
|
-
}
|
|
81
|
-
return undefined;
|
|
75
|
+
blocker(resource, owner) {
|
|
76
|
+
const current = this.owners.get(resource);
|
|
77
|
+
return current && !sameOwner(current, owner) ? current : undefined;
|
|
82
78
|
}
|
|
83
|
-
claim(resource, owner
|
|
84
|
-
|
|
85
|
-
if (this.blocker(resource, owner, now))
|
|
79
|
+
claim(resource, owner) {
|
|
80
|
+
if (this.blocker(resource, owner))
|
|
86
81
|
return false;
|
|
87
82
|
const existing = this.owners.get(resource);
|
|
88
|
-
if (existing && sameOwner(existing
|
|
89
|
-
// A repeated claim by the sitting owner changes nothing: replacing
|
|
90
|
-
// the record would restart activityAt, and re-claims alone would
|
|
91
|
-
// hold the seat past every quiet window (#1653).
|
|
83
|
+
if (existing && sameOwner(existing, owner))
|
|
92
84
|
return true;
|
|
93
|
-
}
|
|
94
85
|
// The free seat belongs to the front waiter; a current holder's
|
|
95
86
|
// revalidation above never loses its own claim to this queue.
|
|
96
87
|
const queue = this.waiters.get(resource);
|
|
@@ -101,76 +92,12 @@ export class TurnResources {
|
|
|
101
92
|
if (!queue.length)
|
|
102
93
|
this.waiters.delete(resource);
|
|
103
94
|
}
|
|
104
|
-
|
|
105
|
-
if (options.idle) {
|
|
106
|
-
record.idle = options.idle;
|
|
107
|
-
// Taking (or retaking) a seat is itself screen activity: the quiet
|
|
108
|
-
// window of #1653 starts at the claim, not at the first later call.
|
|
109
|
-
record.activityAt = now;
|
|
110
|
-
}
|
|
111
|
-
this.owners.set(resource, record);
|
|
112
|
-
const reclaimed = this.reclaims.get(resource);
|
|
113
|
-
// Seating an owner closes any idle-release record for this resource:
|
|
114
|
-
// the previous holder re-claimed (its turn lives on), or another turn
|
|
115
|
-
// took the free seat — an occupied seat is exactly where #1653 says
|
|
116
|
-
// the old holder yields.
|
|
117
|
-
if (reclaimed)
|
|
118
|
-
this.reclaims.delete(resource);
|
|
95
|
+
this.owners.set(resource, owner);
|
|
119
96
|
return true;
|
|
120
97
|
}
|
|
121
|
-
owns(resource, owner
|
|
122
|
-
this.expireIdle(resource, now);
|
|
123
|
-
const current = this.owners.get(resource);
|
|
124
|
-
return Boolean(current && sameOwner(current.owner, owner));
|
|
125
|
-
}
|
|
126
|
-
/** A real screen call touched this claim (#1653): restart its quiet
|
|
127
|
-
* window. Claims without an idle policy — and anything the screen
|
|
128
|
-
* poller drives — are no-ops, so preview frames can never hold a seat. */
|
|
129
|
-
activity(resource, owner, now = Date.now()) {
|
|
130
|
-
// Expiry first: a straggler screen completion arriving at or past the
|
|
131
|
-
// quiet boundary must release the seat and open the reclaim window,
|
|
132
|
-
// not restart the clock on a claim that already lapsed.
|
|
133
|
-
this.expireIdle(resource, now);
|
|
98
|
+
owns(resource, owner) {
|
|
134
99
|
const current = this.owners.get(resource);
|
|
135
|
-
|
|
136
|
-
current.activityAt = now;
|
|
137
|
-
}
|
|
138
|
-
/** A screen-touching computer tool call started on this claim (#1653):
|
|
139
|
-
* the desktop is executing real I/O for the sitting owner, so the seat
|
|
140
|
-
* cannot be idle-released mid-call. Only the sitting owner's start
|
|
141
|
-
* counts; a start whose completion never arrives holds the seat until
|
|
142
|
-
* settle — the pre-#1653 behavior, never an overlapping handoff. */
|
|
143
|
-
beginComputerCall(resource, owner) {
|
|
144
|
-
const current = this.owners.get(resource);
|
|
145
|
-
if (current?.idle && sameOwner(current.owner, owner)) {
|
|
146
|
-
current.computerCallsInFlight = (current.computerCallsInFlight ?? 0) + 1;
|
|
147
|
-
}
|
|
148
|
-
}
|
|
149
|
-
/** The in-flight computer call finished: drop its fence and let the
|
|
150
|
-
* completion's own activity tick govern the quiet window. A no-op when
|
|
151
|
-
* the claim lapsed or another turn seated itself in between. */
|
|
152
|
-
endComputerCall(resource, owner) {
|
|
153
|
-
const current = this.owners.get(resource);
|
|
154
|
-
if (!current?.idle || !sameOwner(current.owner, owner))
|
|
155
|
-
return;
|
|
156
|
-
const inFlight = current.computerCallsInFlight ?? 0;
|
|
157
|
-
if (inFlight > 0)
|
|
158
|
-
current.computerCallsInFlight = inFlight - 1;
|
|
159
|
-
}
|
|
160
|
-
/** Who may still re-claim `resource` directly after an idle release
|
|
161
|
-
* (#1653): the mid-task turn that went quiet, until its reclaim window
|
|
162
|
-
* ends or another turn seats itself. The direct re-claim itself is
|
|
163
|
-
* just `claim`: a free seat answers the returning holder's next screen
|
|
164
|
-
* call without the wait a new arrival would enter. */
|
|
165
|
-
reclaimHolder(resource, now = Date.now()) {
|
|
166
|
-
const reclaimed = this.reclaims.get(resource);
|
|
167
|
-
if (!reclaimed)
|
|
168
|
-
return undefined;
|
|
169
|
-
if (reclaimed.until <= now) {
|
|
170
|
-
this.reclaims.delete(resource);
|
|
171
|
-
return undefined;
|
|
172
|
-
}
|
|
173
|
-
return reclaimed.owner;
|
|
100
|
+
return Boolean(current && sameOwner(current, owner));
|
|
174
101
|
}
|
|
175
102
|
release(owner) {
|
|
176
103
|
for (const [key, queue] of this.waiters) {
|
|
@@ -181,81 +108,23 @@ export class TurnResources {
|
|
|
181
108
|
this.waiters.delete(key);
|
|
182
109
|
}
|
|
183
110
|
for (const [key, current] of this.owners) {
|
|
184
|
-
if (sameOwner(current
|
|
111
|
+
if (sameOwner(current, owner))
|
|
185
112
|
this.owners.delete(key);
|
|
186
113
|
}
|
|
187
|
-
// A settling turn keeps no reclaim priority (#1653): only a live
|
|
188
|
-
// mid-task turn may pick its seat back up.
|
|
189
|
-
for (const [key, reclaimed] of this.reclaims) {
|
|
190
|
-
if (sameOwner(reclaimed.owner, owner))
|
|
191
|
-
this.reclaims.delete(key);
|
|
192
|
-
}
|
|
193
114
|
}
|
|
194
115
|
/** Drop one of an owner's claims early, when the sequence that took it
|
|
195
116
|
* could not finish. The owner's other claims stand until settle. */
|
|
196
|
-
releaseOne(resource, owner
|
|
197
|
-
if (this.owns(resource, owner
|
|
117
|
+
releaseOne(resource, owner) {
|
|
118
|
+
if (this.owns(resource, owner))
|
|
198
119
|
this.owners.delete(resource);
|
|
199
120
|
this.stopWaiting(resource, owner);
|
|
200
|
-
// owns() may have just expired a quiet claim into a reclaim record
|
|
201
|
-
// for this same owner: an early release keeps no reclaim priority.
|
|
202
|
-
const reclaimed = this.reclaims.get(resource);
|
|
203
|
-
if (reclaimed && sameOwner(reclaimed.owner, owner))
|
|
204
|
-
this.reclaims.delete(resource);
|
|
205
|
-
}
|
|
206
|
-
/** Quiet-window expiry (#1653), evaluated when the claim is touched: a
|
|
207
|
-
* screen-quiet seat is released while its turn lives and remembered for
|
|
208
|
-
* the reclaim window. Synchronous on purpose — the claim map decides
|
|
209
|
-
* every transition on the server's single thread. */
|
|
210
|
-
expireIdle(resource, now) {
|
|
211
|
-
const current = this.owners.get(resource);
|
|
212
|
-
if (!current?.idle || current.activityAt === undefined)
|
|
213
|
-
return;
|
|
214
|
-
// A call the desktop is still executing fences the seat (#1653):
|
|
215
|
-
// releasing mid-call would hand one desktop to two turns at once.
|
|
216
|
-
if ((current.computerCallsInFlight ?? 0) > 0)
|
|
217
|
-
return;
|
|
218
|
-
if (!current.idle.quietElapsed(current.activityAt, now))
|
|
219
|
-
return;
|
|
220
|
-
this.owners.delete(resource);
|
|
221
|
-
this.reclaims.set(resource, { owner: current.owner, until: now + current.idle.reclaimMs });
|
|
222
121
|
}
|
|
223
122
|
/** Whether any live owner holds this resource: the parked-resume drain's
|
|
224
123
|
* gate (#1651) — a resume fires only when the seat it queued on is free. */
|
|
225
|
-
free(resource
|
|
226
|
-
|
|
227
|
-
if (!overlaps(key, resource))
|
|
228
|
-
continue;
|
|
229
|
-
// A quiet-window claim expires the moment anything needs to know
|
|
230
|
-
// whether the seat is free: the parked-resume gate must not wait on
|
|
231
|
-
// a claim whose window already elapsed (#1653).
|
|
232
|
-
this.expireIdle(key, now);
|
|
233
|
-
if (this.owners.has(key))
|
|
234
|
-
return false;
|
|
235
|
-
}
|
|
236
|
-
return true;
|
|
124
|
+
free(resource) {
|
|
125
|
+
return !this.owners.has(resource);
|
|
237
126
|
}
|
|
238
127
|
}
|
|
239
128
|
function sameOwner(a, b) {
|
|
240
129
|
return a.threadId === b.threadId && a.generation === b.generation;
|
|
241
130
|
}
|
|
242
|
-
export function workspaceResource(cwd) {
|
|
243
|
-
// Selected folders must exist before the engine starts. Resolve symlinks
|
|
244
|
-
// and native filename casing so aliases cannot grant two writers to the
|
|
245
|
-
// same project on case-insensitive volumes.
|
|
246
|
-
const canonical = realpathSync.native(resolve(cwd));
|
|
247
|
-
return `workspace:${process.platform === "win32" ? canonical.toLowerCase() : canonical}`;
|
|
248
|
-
}
|
|
249
|
-
function overlaps(a, b) {
|
|
250
|
-
if (a === b)
|
|
251
|
-
return true;
|
|
252
|
-
if (!a.startsWith("workspace:") || !b.startsWith("workspace:"))
|
|
253
|
-
return false;
|
|
254
|
-
const left = a.slice("workspace:".length);
|
|
255
|
-
const right = b.slice("workspace:".length);
|
|
256
|
-
const contains = (parent, child) => {
|
|
257
|
-
const path = relative(parent, child);
|
|
258
|
-
return path === "" || (path !== ".." && !path.startsWith(`..${sep}`) && !path.startsWith(sep) && !/^[A-Za-z]:/.test(path));
|
|
259
|
-
};
|
|
260
|
-
return contains(left, right) || contains(right, left);
|
|
261
|
-
}
|
|
@@ -26,7 +26,9 @@ const promptBytesOf = (value) => {
|
|
|
26
26
|
? { stable: clean(record.stable), volatile: clean(record.volatile) }
|
|
27
27
|
: null;
|
|
28
28
|
};
|
|
29
|
-
|
|
29
|
+
/** The UTC month a row belongs to, `YYYY-MM`: the name of its month file.
|
|
30
|
+
* The decision, admin-activity and decider logs use the same layout. */
|
|
31
|
+
export function monthKey(at) {
|
|
30
32
|
return `${at.getUTCFullYear()}-${String(at.getUTCMonth() + 1).padStart(2, "0")}`;
|
|
31
33
|
}
|
|
32
34
|
export function usageFileFor(dataDir, at) {
|
|
@@ -20,7 +20,7 @@ export const WORKSPACES_DIR = join(DATA_DIR, "workspaces");
|
|
|
20
20
|
export const TASK_WORKSPACES_DIR = join(DATA_DIR, "task-workspaces");
|
|
21
21
|
/** MCP support does not imply native filesystem tools or a local working directory. */
|
|
22
22
|
export function supportsWorkspaceFiles(driverKind) {
|
|
23
|
-
return !["grok", "openai-compat", "minimax", "mistral", "cerebras"
|
|
23
|
+
return !["grok", "openai-compat", "minimax", "mistral", "cerebras"].includes(driverKind);
|
|
24
24
|
}
|
|
25
25
|
/** Default task files are private to the thread, outside the bot's shared
|
|
26
26
|
* memory folder. This is directory organization, not a shell sandbox. */
|
|
@@ -588,9 +588,8 @@ export function writeMemoryTopic(botId, name, text) {
|
|
|
588
588
|
writeFileAtomic(join(workspaceDir(botId), "memory", name), redactSecretsInText(text), { mode: 0o600 });
|
|
589
589
|
indexWrittenMemoryFile(botId, `memory/${name}`);
|
|
590
590
|
}
|
|
591
|
-
/** Read one topic file. The name gate runs here
|
|
592
|
-
*
|
|
593
|
-
* arbitrary path. Null for anything invalid or unreadable. */
|
|
591
|
+
/** Read one topic file. The name gate runs here, so no caller can turn this
|
|
592
|
+
* into a read of an arbitrary path. Null for anything invalid or unreadable. */
|
|
594
593
|
export function readMemoryTopic(botId, name) {
|
|
595
594
|
if (!isMemoryTopicName(name))
|
|
596
595
|
return null;
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
// server/server-launcher.ts
|
|
2
|
+
import { spawn } from "node:child_process";
|
|
3
|
+
import { realpathSync } from "node:fs";
|
|
4
|
+
import { dirname, join, resolve } from "node:path";
|
|
5
|
+
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
6
|
+
|
|
7
|
+
// server/restart.ts
|
|
8
|
+
var RESTART_EXIT_CODE = 75;
|
|
9
|
+
var MAX_RESTARTS = 5;
|
|
10
|
+
var STABLE_RUN_MS = 6e4;
|
|
11
|
+
function restartPolicy(now = Date.now) {
|
|
12
|
+
let restarts = 0, startedAt = now();
|
|
13
|
+
return {
|
|
14
|
+
again(code, stopping = false) {
|
|
15
|
+
if (now() - startedAt >= STABLE_RUN_MS) restarts = 0;
|
|
16
|
+
if (stopping || code !== RESTART_EXIT_CODE || restarts >= MAX_RESTARTS) return false;
|
|
17
|
+
restarts++;
|
|
18
|
+
startedAt = now();
|
|
19
|
+
return true;
|
|
20
|
+
}
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// server/server-launcher.ts
|
|
25
|
+
function superviseServer(start, options = {}) {
|
|
26
|
+
const signals = options.signals ?? process, now = options.now ?? Date.now;
|
|
27
|
+
return new Promise((done) => {
|
|
28
|
+
let child, stopping = false;
|
|
29
|
+
const policy = restartPolicy(now);
|
|
30
|
+
const pass = (signal) => () => {
|
|
31
|
+
stopping = true;
|
|
32
|
+
child.kill(signal);
|
|
33
|
+
};
|
|
34
|
+
const onTerm = pass("SIGTERM"), onInt = pass("SIGINT");
|
|
35
|
+
const finish = (code) => {
|
|
36
|
+
signals.off("SIGTERM", onTerm);
|
|
37
|
+
signals.off("SIGINT", onInt);
|
|
38
|
+
done(code);
|
|
39
|
+
};
|
|
40
|
+
const run = () => {
|
|
41
|
+
child = start();
|
|
42
|
+
child.once("error", () => finish(1));
|
|
43
|
+
child.once("exit", (code, signal) => {
|
|
44
|
+
if (policy.again(code, stopping)) {
|
|
45
|
+
console.log("OpenMausBot is starting again to finish installing a copy from the desktop app\u2026");
|
|
46
|
+
run();
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
finish(code ?? (stopping && (signal === "SIGTERM" || signal === "SIGINT") ? 0 : 1));
|
|
50
|
+
});
|
|
51
|
+
};
|
|
52
|
+
signals.on("SIGTERM", onTerm);
|
|
53
|
+
signals.on("SIGINT", onInt);
|
|
54
|
+
run();
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
function isEntry() {
|
|
58
|
+
try {
|
|
59
|
+
return Boolean(process.argv[1]) && import.meta.url === pathToFileURL(realpathSync(resolve(process.argv[1]))).href;
|
|
60
|
+
} catch {
|
|
61
|
+
return false;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
if (isEntry()) {
|
|
65
|
+
const server = join(dirname(fileURLToPath(import.meta.url)), "index.js");
|
|
66
|
+
void superviseServer(() => spawn(process.execPath, [server], { stdio: "inherit" })).then((code) => {
|
|
67
|
+
process.exitCode = code;
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
export {
|
|
71
|
+
superviseServer
|
|
72
|
+
};
|
|
@@ -34,7 +34,7 @@ export const MAX_CUSTOM_ANSWER = 2000;
|
|
|
34
34
|
export function isPersistentQuestionCard(card) {
|
|
35
35
|
return Boolean(card && (card.requestType === "question" || card.questionRequest ||
|
|
36
36
|
(!card.requestType && !card.tool && !card.routineRequest && !card.skillRequest && !card.profileRequest &&
|
|
37
|
-
!card.modelRequest && !card.
|
|
37
|
+
!card.modelRequest && !card.teamSetupRequest)));
|
|
38
38
|
}
|
|
39
39
|
/** System/provider completion ends the live run, not the human's question. */
|
|
40
40
|
export function shouldSettleRequestCard(card, source) {
|
|
@@ -109,55 +109,6 @@ export function parseAskQuestions(input) {
|
|
|
109
109
|
}
|
|
110
110
|
return questions.length ? questions : null;
|
|
111
111
|
}
|
|
112
|
-
/** The fenced block a turn-boundary agent ends its run with when it wants
|
|
113
|
-
* to ask the person something (the BoatAgent transport): the harness cannot
|
|
114
|
-
* pause mid-run, so the questions ride the final output and OMB parses them
|
|
115
|
-
* at settle. The block is model-authored — untrusted input like any tool
|
|
116
|
-
* call — so its body runs through parseAskQuestions and the same caps. */
|
|
117
|
-
const 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|$)/;
|
|
118
|
-
const OMB_ASK_FENCE_GLOBAL = new RegExp(OMB_ASK_FENCE.source, OMB_ASK_FENCE.flags + "g");
|
|
119
|
-
/** The questions inside the first fenced omb-ask block in an output, or
|
|
120
|
-
* null when there is nothing worth showing (no fence, invalid JSON, or no
|
|
121
|
-
* entry that parses as a question). The first fence wins: a second block
|
|
122
|
-
* in the same output is ignored, not merged. */
|
|
123
|
-
export function parseOmbAskQuestions(output) {
|
|
124
|
-
const match = OMB_ASK_FENCE.exec(output);
|
|
125
|
-
if (!match)
|
|
126
|
-
return null;
|
|
127
|
-
try {
|
|
128
|
-
return parseAskQuestions(JSON.parse(match[3]));
|
|
129
|
-
}
|
|
130
|
-
catch {
|
|
131
|
-
return null;
|
|
132
|
-
}
|
|
133
|
-
}
|
|
134
|
-
/** The output with its omb-ask block(s) removed, for display: the person
|
|
135
|
-
* reads the prose around the ask, never the raw protocol JSON. Text
|
|
136
|
-
* without a block is returned untouched. */
|
|
137
|
-
export function stripOmbAskBlock(output) {
|
|
138
|
-
if (!OMB_ASK_FENCE.test(output))
|
|
139
|
-
return output;
|
|
140
|
-
return output.replace(OMB_ASK_FENCE_GLOBAL, "").replace(/\n{3,}/g, "\n\n").trim();
|
|
141
|
-
}
|
|
142
|
-
/** The prompt contract that teaches a model the turn-held ask transport: end
|
|
143
|
-
* the reply with a fenced `omb-ask` block carrying the questions, and the
|
|
144
|
-
* answers come back on the next prompt. A harness that cannot pause mid-run
|
|
145
|
-
* (the BoatAgent transport) appends this to every prompt; the caps in the text
|
|
146
|
-
* are the constants above, so the taught contract and the parser cannot
|
|
147
|
-
* drift apart. */
|
|
148
|
-
export function ombAskProtocolPrompt() {
|
|
149
|
-
return [
|
|
150
|
-
"",
|
|
151
|
-
"## Asking the person a question",
|
|
152
|
-
"When a decision belongs to the person, end your reply with a fenced block exactly like this:",
|
|
153
|
-
"",
|
|
154
|
-
"```omb-ask",
|
|
155
|
-
'{"questions":[{"question":"Ship the release now?","header":"Release","options":[{"label":"Ship now"},{"label":"Wait for the QA signoff"}]}]}',
|
|
156
|
-
"```",
|
|
157
|
-
"",
|
|
158
|
-
`The block must be the last thing in your reply. You may ask up to ${MAX_QUESTIONS} questions at once, each with up to ${MAX_OPTIONS} options; the person can always answer in their own words. Their answers arrive on your next prompt as \`Q:\`/\`A:\` lines — never invent them.`,
|
|
159
|
-
].join("\n");
|
|
160
|
-
}
|
|
161
112
|
/** The one line the card subtitle and a spoken prompt show. */
|
|
162
113
|
export function askQuestionSummary(questions) {
|
|
163
114
|
const first = questions[0]?.question ?? "";
|
|
@@ -254,10 +205,6 @@ export const ASK_USER_TOOL_DEFINITION = {
|
|
|
254
205
|
},
|
|
255
206
|
},
|
|
256
207
|
};
|
|
257
|
-
/** The synthetic tool string for an ask parsed out of model-authored final
|
|
258
|
-
* output (the BoatAgent turn-held transport): there is no tool call to name,
|
|
259
|
-
* but the event and the ASKS_A_PERSON backstop need one string. */
|
|
260
|
-
export const OMB_ASK_TOOL = "omb-ask";
|
|
261
208
|
/** The lead-in on a formatted answer. It exists for the model — the answer
|
|
262
209
|
* is delivered on the deny channel, so it has to say what it is — and the
|
|
263
210
|
* card strips it back off when it shows the person what they sent. */
|
|
@@ -283,23 +230,6 @@ export function formatQuestionAnswers(questions, answers) {
|
|
|
283
230
|
export function answerWithoutPreamble(answer) {
|
|
284
231
|
return answer.startsWith(`${ANSWER_PREAMBLE}\n\n`) ? answer.slice(ANSWER_PREAMBLE.length + 2) : answer;
|
|
285
232
|
}
|
|
286
|
-
/** The longest formatted answer OMB will echo back into a follow-up prompt
|
|
287
|
-
* (the BoatAgent continuation). formatQuestionAnswers itself does not
|
|
288
|
-
* truncate — the tool-result channel has no stated limit — but a prompt is
|
|
289
|
-
* not the place to find one: six answers at the custom-answer cap is the
|
|
290
|
-
* most a legitimate reply weighs, so that is the ceiling. */
|
|
291
|
-
export const MAX_ANSWER_ECHO = 6 * MAX_CUSTOM_ANSWER;
|
|
292
|
-
/** Cap a formatted answer for echoing into a follow-up prompt. An over-cap
|
|
293
|
-
* echo is cut back to the last whole block so no partial answer reads as
|
|
294
|
-
* one, and says it was truncated. */
|
|
295
|
-
export function capAnswerEcho(answer, limit = MAX_ANSWER_ECHO) {
|
|
296
|
-
if (answer.length <= limit)
|
|
297
|
-
return answer;
|
|
298
|
-
const cut = answer.slice(0, limit);
|
|
299
|
-
const boundary = cut.lastIndexOf("\n\nQ: ");
|
|
300
|
-
const kept = boundary > 0 ? cut.slice(0, boundary) : cut;
|
|
301
|
-
return kept + "\n\n[answer truncated]";
|
|
302
|
-
}
|
|
303
233
|
/**
|
|
304
234
|
* The answer text, read back as one value per question.
|
|
305
235
|
*
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
// Which engines can work on a cloud computer: Hosted desktop on Boat, or a
|
|
2
|
+
// self-hosted VPS. One rule for the server (every attach, readiness check and
|
|
3
|
+
// select_computer offer) and the renderer (the Works on panel, the composer's
|
|
4
|
+
// place chip, Webhooks and Routines), so the two never disagree.
|
|
5
|
+
/** An engine with computer tools gets the cloud computer as one more stdio
|
|
6
|
+
* computer server, on a Boat and on a VPS alike. The turn always stays on the
|
|
7
|
+
* bot's own engine; nothing hands it to another one. */
|
|
8
|
+
export function canWorkOnCloud(engine) {
|
|
9
|
+
return engine?.computerMcp === true;
|
|
10
|
+
}
|
|
@@ -18,7 +18,7 @@ export const CREDENTIAL_TARGETS = {
|
|
|
18
18
|
},
|
|
19
19
|
opencodeGoApiKey: {
|
|
20
20
|
label: "OpenCode API key",
|
|
21
|
-
description: "Used for OpenCode
|
|
21
|
+
description: "Used for OpenCode Zen and Go.",
|
|
22
22
|
placeholder: "Paste your OpenCode API key",
|
|
23
23
|
helpUrl: "https://opencode.ai/docs/providers/",
|
|
24
24
|
},
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
const MARKER = "error:";
|
|
2
|
+
/** The longest cause a row keeps. Clients wrap the row and show it whole, so
|
|
3
|
+
* this only bounds a pathological message (a provider's HTML body, a stack
|
|
4
|
+
* trace); a sentence a person acts on fits, its next action included. */
|
|
5
|
+
export const FAILED_TURN_MAX_CHARS = 600;
|
|
6
|
+
/** The activity row a failed turn is stored as. */
|
|
7
|
+
export function failedTurnTool(cause, flags = {}) {
|
|
8
|
+
const words = cause.length > FAILED_TURN_MAX_CHARS ? `${cause.slice(0, FAILED_TURN_MAX_CHARS - 1)}…` : cause;
|
|
9
|
+
return {
|
|
10
|
+
name: `${MARKER} ${words}`,
|
|
11
|
+
ok: false,
|
|
12
|
+
...(flags.setup ? { setup: true } : {}),
|
|
13
|
+
...(flags.terminal ? { terminal: true } : {}),
|
|
14
|
+
...(flags.claudeUpdate ? { claudeUpdate: true } : {}),
|
|
15
|
+
...(flags.place ? { place: flags.place } : {}),
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
/** The cause a failed-turn row carries, without its marker; null for any
|
|
19
|
+
* other activity row. */
|
|
20
|
+
export function failedTurnCause(name) {
|
|
21
|
+
return name.startsWith(MARKER) ? name.slice(MARKER.length).trim() : null;
|
|
22
|
+
}
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
/** "approval": a provider or peer ask the voice may decide with a strict yes/no.
|
|
2
2
|
* "review": a harness-native proposal (skill, routine, profile, default
|
|
3
|
-
* model,
|
|
3
|
+
* model, team setup) that must be reviewed on screen.
|
|
4
4
|
* "question": a provider question. An expired proposal is settled: nothing
|
|
5
5
|
* can answer it any more. */
|
|
6
6
|
export function liveCardKind(card) {
|
|
7
7
|
if (!card?.requestId || card.answered || card.dismissed || card.expired)
|
|
8
8
|
return null;
|
|
9
|
-
if (card.skillRequest || card.routineRequest || card.profileRequest || card.modelRequest || card.
|
|
9
|
+
if (card.skillRequest || card.routineRequest || card.profileRequest || card.modelRequest || card.teamSetupRequest) {
|
|
10
10
|
return "review";
|
|
11
11
|
}
|
|
12
12
|
return card.tool ? "approval" : "question";
|
|
@@ -55,9 +55,9 @@ const encoder = new TextEncoder();
|
|
|
55
55
|
export function utf8Bytes(value) {
|
|
56
56
|
return encoder.encode(value).length;
|
|
57
57
|
}
|
|
58
|
-
// ── shared field helpers (
|
|
59
|
-
const requiredText = (max) => z.string({ error: "must be text" }).trim().min(1, { message: "is required" }).max(max, { message: "is too long" });
|
|
60
|
-
const optionalText = (max) => z
|
|
58
|
+
// ── shared field helpers (the team manifest reader uses these too) ────────
|
|
59
|
+
export const requiredText = (max) => z.string({ error: "must be text" }).trim().min(1, { message: "is required" }).max(max, { message: "is too long" });
|
|
60
|
+
export const optionalText = (max) => z
|
|
61
61
|
.union([z.string({ error: "must be text" }), z.null(), z.undefined()])
|
|
62
62
|
.transform((value) => value?.trim() || undefined)
|
|
63
63
|
.refine((value) => value === undefined || value.length <= max, { message: "is too long" })
|