@brantrusnak/openclaw-omadeus 1.0.2 → 1.0.3

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.
Files changed (40) hide show
  1. package/README.md +4 -1
  2. package/dist/_virtual/_rolldown/runtime.js +4 -0
  3. package/dist/api.js +5 -0
  4. package/dist/index.js +14 -0
  5. package/dist/runtime-api.js +15 -0
  6. package/dist/setup-entry.js +7 -0
  7. package/dist/src/allowed-reaction-emojis.js +21 -0
  8. package/dist/src/api/auth.api.js +115 -0
  9. package/dist/src/api/channel.api.js +23 -0
  10. package/dist/src/api/message.api.js +76 -0
  11. package/dist/src/api/nugget.api.js +127 -0
  12. package/dist/src/auth.js +30 -0
  13. package/dist/src/channel.js +626 -0
  14. package/dist/src/config.js +52 -0
  15. package/dist/src/defaults.js +5 -0
  16. package/dist/src/inbound-policy.js +205 -0
  17. package/dist/src/inbound.js +97 -0
  18. package/dist/src/member-resolve.js +53 -0
  19. package/dist/src/message-handler.js +262 -0
  20. package/dist/src/nugget-lookup.js +140 -0
  21. package/dist/src/onboarding.js +363 -0
  22. package/dist/src/outbound.js +17 -0
  23. package/dist/src/reply-dispatcher.js +46 -0
  24. package/dist/src/runtime.js +5 -0
  25. package/dist/src/setup-core.js +46 -0
  26. package/dist/src/setup-surface.js +2 -0
  27. package/dist/src/socket/dolphin.socket.js +18 -0
  28. package/dist/src/socket/jaguar.socket.js +22 -0
  29. package/dist/src/socket/socket.js +153 -0
  30. package/dist/src/store.js +13 -0
  31. package/dist/src/token.js +84 -0
  32. package/dist/src/types.js +15 -0
  33. package/dist/src/utils/http.util.js +43 -0
  34. package/dist/src/utils/jwt.util.js +15 -0
  35. package/package.json +10 -3
  36. package/src/channel.ts +127 -238
  37. package/src/member-resolve.ts +1 -1
  38. package/src/onboarding.ts +71 -110
  39. package/src/setup-core.ts +10 -1
  40. package/src/socket/socket.ts +24 -11
@@ -0,0 +1,205 @@
1
+ import { OMADEUS_INBOUND_ENTITY_KIND_SET } from "./types.js";
2
+ //#region src/inbound-policy.ts
3
+ /** Default inbound policy when `channels.omadeus.inbound` is absent. */
4
+ const DEFAULT_INBOUND_POLICY = {
5
+ version: 1,
6
+ direct: {
7
+ enabled: true,
8
+ requireMention: "never"
9
+ },
10
+ channels: {
11
+ enabled: false,
12
+ requireMention: "outsideAllowlist"
13
+ },
14
+ entities: {
15
+ enabled: false,
16
+ requireMention: "always"
17
+ }
18
+ };
19
+ function mergePolicy(cfg) {
20
+ const inbound = cfg?.inbound;
21
+ return {
22
+ version: typeof inbound?.version === "number" && inbound.version >= 1 ? inbound.version : 1,
23
+ direct: {
24
+ ...DEFAULT_INBOUND_POLICY.direct,
25
+ ...inbound?.direct,
26
+ requireMention: inbound?.direct?.requireMention ?? DEFAULT_INBOUND_POLICY.direct.requireMention
27
+ },
28
+ channels: {
29
+ ...DEFAULT_INBOUND_POLICY.channels,
30
+ ...inbound?.channels,
31
+ requireMention: inbound?.channels?.requireMention ?? DEFAULT_INBOUND_POLICY.channels.requireMention
32
+ },
33
+ entities: {
34
+ ...DEFAULT_INBOUND_POLICY.entities,
35
+ ...inbound?.entities,
36
+ requireMention: inbound?.entities?.requireMention ?? DEFAULT_INBOUND_POLICY.entities.requireMention
37
+ }
38
+ };
39
+ }
40
+ function surfaceForKind(kind) {
41
+ if (kind === "direct") return "direct";
42
+ if (kind === "channel") return "channel";
43
+ return "entity";
44
+ }
45
+ function senderAllowed(allowed, fromReferenceId) {
46
+ if (!allowed || allowed.length === 0) return true;
47
+ return allowed.includes(fromReferenceId);
48
+ }
49
+ function channelGeoAllowed(params) {
50
+ const { roomId, channelViewId, allowedRoomIds = [], allowedChannelViewIds = [] } = params;
51
+ const hasRooms = allowedRoomIds.length > 0;
52
+ const hasViews = allowedChannelViewIds.length > 0;
53
+ let geoInAllowlist = true;
54
+ if (hasRooms && hasViews) geoInAllowlist = allowedRoomIds.includes(roomId) || channelViewId !== void 0 && allowedChannelViewIds.includes(channelViewId);
55
+ else if (hasRooms) geoInAllowlist = allowedRoomIds.includes(roomId);
56
+ else if (hasViews) geoInAllowlist = channelViewId !== void 0 && allowedChannelViewIds.includes(channelViewId);
57
+ return {
58
+ geoInAllowlist,
59
+ details: {
60
+ roomId,
61
+ channelViewId,
62
+ allowedRoomIds,
63
+ allowedChannelViewIds,
64
+ hasRooms,
65
+ hasViews,
66
+ geoInAllowlist
67
+ }
68
+ };
69
+ }
70
+ function entityKindAllowed(kind, allowedKinds) {
71
+ if (!allowedKinds || allowedKinds.length === 0) return OMADEUS_INBOUND_ENTITY_KIND_SET.has(String(kind));
72
+ return allowedKinds.includes(String(kind));
73
+ }
74
+ function entityRoomOk(roomId, allowedRoomIds) {
75
+ if (!allowedRoomIds || allowedRoomIds.length === 0) return true;
76
+ return allowedRoomIds.includes(roomId);
77
+ }
78
+ function mentionRequired(params) {
79
+ const requireMention = params.requireMention ?? "never";
80
+ const { inAllowlist, isMention } = params;
81
+ if (requireMention === "never") return false;
82
+ if (requireMention === "always") return !isMention;
83
+ if (inAllowlist) return false;
84
+ return !isMention;
85
+ }
86
+ /**
87
+ * Evaluate whether a normalized Jaguar inbound should be dispatched to OpenClaw.
88
+ * Callers must drop self-authored messages separately if they prefer logging there.
89
+ */
90
+ function evaluateOmadeusInboundPolicy(params) {
91
+ const { inbound, omadeusCfg, selfReferenceId } = params;
92
+ if (inbound.fromReferenceId === selfReferenceId) return {
93
+ allow: false,
94
+ reason: "self_message",
95
+ details: {
96
+ fromReferenceId: inbound.fromReferenceId,
97
+ selfReferenceId
98
+ }
99
+ };
100
+ const policy = mergePolicy(omadeusCfg);
101
+ const surface = surfaceForKind(inbound.subscribableKind);
102
+ if (surface === "direct") {
103
+ if (!policy.direct.enabled) return {
104
+ allow: false,
105
+ reason: "direct_disabled",
106
+ details: { surface }
107
+ };
108
+ if (!senderAllowed(policy.direct.allowedSenderReferenceIds, inbound.fromReferenceId)) return {
109
+ allow: false,
110
+ reason: "direct_sender_not_allowed",
111
+ details: { fromReferenceId: inbound.fromReferenceId }
112
+ };
113
+ const req = policy.direct.requireMention ?? "never";
114
+ if (mentionRequired({
115
+ requireMention: req,
116
+ inAllowlist: true,
117
+ isMention: inbound.isMention
118
+ })) return {
119
+ allow: false,
120
+ reason: "direct_mention_required",
121
+ details: { requireMention: req }
122
+ };
123
+ return { allow: true };
124
+ }
125
+ if (surface === "channel") {
126
+ if (!policy.channels.enabled) return {
127
+ allow: false,
128
+ reason: "channels_disabled",
129
+ details: { surface }
130
+ };
131
+ if (!senderAllowed(policy.channels.allowedSenderReferenceIds, inbound.fromReferenceId)) return {
132
+ allow: false,
133
+ reason: "channel_sender_not_allowed",
134
+ details: { fromReferenceId: inbound.fromReferenceId }
135
+ };
136
+ const rv = channelGeoAllowed({
137
+ roomId: inbound.roomId,
138
+ channelViewId: inbound.channelViewId,
139
+ allowedRoomIds: policy.channels.allowedRoomIds,
140
+ allowedChannelViewIds: policy.channels.allowedChannelViewIds
141
+ });
142
+ const senderInList = !policy.channels.allowedSenderReferenceIds || policy.channels.allowedSenderReferenceIds.length === 0 || policy.channels.allowedSenderReferenceIds.includes(inbound.fromReferenceId);
143
+ const inAllowlist = rv.geoInAllowlist && senderInList;
144
+ const channelMention = policy.channels.requireMention ?? DEFAULT_INBOUND_POLICY.channels.requireMention;
145
+ if (mentionRequired({
146
+ requireMention: channelMention,
147
+ inAllowlist,
148
+ isMention: inbound.isMention
149
+ })) return {
150
+ allow: false,
151
+ reason: "channel_mention_required",
152
+ details: {
153
+ requireMention: channelMention,
154
+ inAllowlist,
155
+ isMention: inbound.isMention
156
+ }
157
+ };
158
+ return { allow: true };
159
+ }
160
+ if (!policy.entities.enabled) return {
161
+ allow: false,
162
+ reason: "entities_disabled",
163
+ details: { kind: inbound.subscribableKind }
164
+ };
165
+ if (!entityKindAllowed(inbound.subscribableKind, policy.entities.allowedKinds)) return {
166
+ allow: false,
167
+ reason: "entity_kind_not_allowed",
168
+ details: {
169
+ kind: inbound.subscribableKind,
170
+ allowedKinds: policy.entities.allowedKinds
171
+ }
172
+ };
173
+ if (!senderAllowed(policy.entities.allowedSenderReferenceIds, inbound.fromReferenceId)) return {
174
+ allow: false,
175
+ reason: "entity_sender_not_allowed",
176
+ details: { fromReferenceId: inbound.fromReferenceId }
177
+ };
178
+ if (!entityRoomOk(inbound.roomId, policy.entities.allowedRoomIds)) return {
179
+ allow: false,
180
+ reason: "entity_room_not_allowed",
181
+ details: {
182
+ roomId: inbound.roomId,
183
+ allowedRoomIds: policy.entities.allowedRoomIds
184
+ }
185
+ };
186
+ const roomList = policy.entities.allowedRoomIds ?? [];
187
+ const inAllowlist = roomList.length === 0 || roomList.includes(inbound.roomId);
188
+ const entityMention = policy.entities.requireMention ?? DEFAULT_INBOUND_POLICY.entities.requireMention;
189
+ if (mentionRequired({
190
+ requireMention: entityMention,
191
+ inAllowlist,
192
+ isMention: inbound.isMention
193
+ })) return {
194
+ allow: false,
195
+ reason: "entity_mention_required",
196
+ details: {
197
+ requireMention: entityMention,
198
+ inAllowlist,
199
+ isMention: inbound.isMention
200
+ }
201
+ };
202
+ return { allow: true };
203
+ }
204
+ //#endregion
205
+ export { evaluateOmadeusInboundPolicy };
@@ -0,0 +1,97 @@
1
+ //#region src/inbound.ts
2
+ const USER_REF_PATTERN = /\{user_reference_id:(\d+)\}/g;
3
+ const BOLD_MENTION_PREFIX_PATTERN = /^\*\*@[^*]+\*\*/;
4
+ /**
5
+ * Parse the `details` JSON string to extract the rawMessage with mention
6
+ * template tokens like `{user_reference_id:87}`.
7
+ */
8
+ function parseDetails(raw) {
9
+ if (!raw) return null;
10
+ try {
11
+ return JSON.parse(raw);
12
+ } catch {
13
+ return null;
14
+ }
15
+ }
16
+ /**
17
+ * Check whether the bot user (by referenceId) is @-mentioned in the message.
18
+ * Omadeus encodes mentions as `{user_reference_id:N}` in details.rawMessage.
19
+ */
20
+ function isBotMentioned(details, selfReferenceId) {
21
+ const raw = details?.rawMessage;
22
+ if (!raw) return false;
23
+ for (const match of raw.matchAll(USER_REF_PATTERN)) if (Number(match[1]) === selfReferenceId) return true;
24
+ return false;
25
+ }
26
+ /**
27
+ * Fallback mention detection when details.rawMessage is absent.
28
+ * Omadeus often prefixes mentioned messages with `**@Display Name** ...`.
29
+ */
30
+ function hasMentionPrefixInBody(body) {
31
+ return BOLD_MENTION_PREFIX_PATTERN.test(body.trim());
32
+ }
33
+ /**
34
+ * Strip the formatted @mention from the body so the agent sees clean text.
35
+ * The body contains `**@Display Name** actual text`; this strips the bold
36
+ * mention prefix when it appears at the start.
37
+ */
38
+ function stripLeadingMention(body) {
39
+ return body.replace(/^\*\*@[^*]+\*\*\s*/, "").trim();
40
+ }
41
+ function readChannelViewId(metadata) {
42
+ if (metadata === null || metadata === void 0 || typeof metadata !== "object") return;
43
+ const m = metadata;
44
+ for (const key of [
45
+ "channelViewId",
46
+ "channel_view_id",
47
+ "viewId",
48
+ "subscribableViewId"
49
+ ]) {
50
+ const v = m[key];
51
+ if (typeof v === "number" && Number.isFinite(v)) return v;
52
+ if (typeof v === "string" && /^\d+$/.test(v.trim())) return Number(v.trim());
53
+ }
54
+ }
55
+ /**
56
+ * Determine whether a raw Jaguar socket payload is an OmadeusMessage.
57
+ */
58
+ function isOmadeusMessage(data) {
59
+ if (typeof data !== "object" || data === null) return false;
60
+ const obj = data;
61
+ return obj.type === "message" && typeof obj.roomId === "number" && typeof obj.body === "string";
62
+ }
63
+ /**
64
+ * Parse a Jaguar socket message into an OpenClaw inbound message.
65
+ *
66
+ * Returns null when:
67
+ * - The event is not a chat message
68
+ * - The body is empty or the message was removed
69
+ */
70
+ function parseJaguarMessage(msg, opts, log) {
71
+ if (msg.type !== "message") {
72
+ log?.debug?.(`[jaguar-inbound] ignoring type: ${msg.type}`);
73
+ return null;
74
+ }
75
+ if (msg.removedAt) return null;
76
+ const body = (msg.body ?? "").trim();
77
+ if (!body) return null;
78
+ const mentioned = isBotMentioned(parseDetails(msg.details), opts.selfReferenceId) || hasMentionPrefixInBody(body);
79
+ const content = mentioned ? stripLeadingMention(body) : body;
80
+ if (!content) return null;
81
+ const channelViewId = readChannelViewId(msg.metadata);
82
+ return {
83
+ messageId: msg.id,
84
+ from: String(msg.senderReferenceId),
85
+ fromReferenceId: msg.senderReferenceId,
86
+ content,
87
+ roomId: msg.roomId,
88
+ roomName: msg.roomName,
89
+ subscribableType: msg.subscribableType,
90
+ subscribableKind: msg.subscribableKind,
91
+ ...channelViewId !== void 0 ? { channelViewId } : {},
92
+ isMention: mentioned,
93
+ timestamp: msg.createdAtTimestamp ? Math.floor(msg.createdAtTimestamp * 1e3) : Date.now()
94
+ };
95
+ }
96
+ //#endregion
97
+ export { isOmadeusMessage, parseJaguarMessage };
@@ -0,0 +1,53 @@
1
+ import { listOrganizationMembers } from "./api/auth.api.js";
2
+ //#region src/member-resolve.ts
3
+ function formatMemberLabel(m) {
4
+ const fullName = `${m.firstName ?? ""} ${m.lastName ?? ""}`.trim();
5
+ if (fullName) return fullName;
6
+ if (m.title?.trim()) return m.title.trim();
7
+ if (m.email?.trim()) return m.email.trim();
8
+ return `Member ${m.referenceId}`;
9
+ }
10
+ /**
11
+ * Resolves Omadeus `referenceId` → human-readable label for the current organization (session JWT).
12
+ * Used so agents do not echo raw ids like 210 to users.
13
+ */
14
+ async function buildReferenceIdNameMap(apiOpts) {
15
+ const { organizationId } = apiOpts.tokenManager.getPayload();
16
+ const sessionToken = apiOpts.tokenManager.getToken();
17
+ const members = await listOrganizationMembers({
18
+ maestroUrl: apiOpts.maestroUrl,
19
+ sessionToken,
20
+ organizationId
21
+ });
22
+ const map = /* @__PURE__ */ new Map();
23
+ for (const m of members) map.set(m.referenceId, formatMemberLabel(m));
24
+ return map;
25
+ }
26
+ /**
27
+ * Adds a `people` object mapping each `*ReferenceId` field in the nugget row to a display name.
28
+ * Keys match the source field names (e.g. `memberReferenceId: "Pat Example"`).
29
+ */
30
+ async function mergePeopleIntoNuggetAgentPayload(apiOpts, fullRecord, basePayload) {
31
+ let nameByRef;
32
+ try {
33
+ nameByRef = await buildReferenceIdNameMap(apiOpts);
34
+ } catch {
35
+ return { ...basePayload };
36
+ }
37
+ const people = {};
38
+ for (const [key, v] of Object.entries(fullRecord)) {
39
+ if (!/referenceid$/i.test(key)) continue;
40
+ const id = typeof v === "number" && Number.isFinite(v) ? v : typeof v === "string" && /^\d+$/.test(v.trim()) ? Number(v.trim()) : NaN;
41
+ if (!Number.isFinite(id)) continue;
42
+ const name = nameByRef.get(id);
43
+ if (name) people[key] = name;
44
+ }
45
+ const out = { ...basePayload };
46
+ if (Object.keys(people).length > 0) {
47
+ out.people = people;
48
+ for (const key of Object.keys(people)) if (key in out) delete out[key];
49
+ }
50
+ return out;
51
+ }
52
+ //#endregion
53
+ export { formatMemberLabel, mergePeopleIntoNuggetAgentPayload };
@@ -0,0 +1,262 @@
1
+ import { logInboundDrop, resolveControlCommandGate } from "../runtime-api.js";
2
+ import { createNugget, findNuggetByTaskChannelRoom, resolveTaskChannelRoomId, searchNuggetByNumber } from "./api/nugget.api.js";
3
+ import { getOmadeusChannelConfig } from "./config.js";
4
+ import { appendNuggetContextForTaskOrNuggetRoom, appendNuggetLookupContextForAgent, parseChannelTaskCreateIntent, parseNuggetLookupIntent, parseRecurringScheduleIntent } from "./nugget-lookup.js";
5
+ import { getOmadeusRuntime } from "./runtime.js";
6
+ import { createOmadeusReplyDispatcher } from "./reply-dispatcher.js";
7
+ import { OMADEUS_INBOUND_ENTITY_KIND_SET } from "./types.js";
8
+ import { evaluateOmadeusInboundPolicy } from "./inbound-policy.js";
9
+ //#region src/message-handler.ts
10
+ const SK = "`subscribableKind`";
11
+ /** Injected into BodyForAgent so the model uses Jaguar room kind, not invented OpenClaw `task/...` keys. */
12
+ function buildOmadeusEntityRoomContextLine(kind) {
13
+ if (kind === "task") return `This chat is an **Omadeus Task** room (${SK} \`task\`). If the user asks about the Task, its status, or says \"the task\", they mean **this** Omadeus Task / this thread — not an OpenClaw \"task\" or a \"session id\" for it.`;
14
+ if (kind === "nugget") return `This chat is an **Omadeus Nugget** room (${SK} \`nugget\`). If the user asks about the Nugget, its status, or colloquially says \"the task\", they mean **this** Omadeus Nugget / this thread — not an OpenClaw \"task\" or a \"session id\" for it. (Task and Nugget are different; infer from this room's ${SK}.)`;
15
+ const label = {
16
+ project: "Project",
17
+ release: "Release",
18
+ sprint: "Sprint",
19
+ summary: "Summary",
20
+ client: "Client",
21
+ folder: "Folder"
22
+ }[kind] ?? kind;
23
+ return `This chat is an **Omadeus ${label}** room (${SK} \`${kind}\`). User questions refer to that Omadeus ${label} in this thread — not an OpenClaw \"session id\" for it.`;
24
+ }
25
+ function shouldSkipTaskRoomNuggetFetch(rawBody) {
26
+ const t = rawBody.trim();
27
+ if (!t) return true;
28
+ if (/^(ok|k|thanks?|thank you|ty|sounds good|got it|yes|yep|no|nope)\s*!*\.?$/i.test(t)) return true;
29
+ return false;
30
+ }
31
+ function createOmadeusMessageHandler(deps) {
32
+ const { cfg, runtime, log, outboundDeps, selfReferenceId } = deps;
33
+ const core = getOmadeusRuntime();
34
+ const omadeusCfg = getOmadeusChannelConfig(cfg);
35
+ const inboundDebounceMs = core.channel.debounce.resolveInboundDebounceMs({
36
+ cfg,
37
+ channel: "omadeus"
38
+ });
39
+ const handleMessageNow = async (inbound) => {
40
+ const isDirectMessage = inbound.subscribableKind === "direct";
41
+ const senderId = String(inbound.fromReferenceId);
42
+ const senderName = inbound.from;
43
+ const roomId = String(inbound.roomId);
44
+ const rawBody = inbound.content;
45
+ if (!rawBody.trim()) {
46
+ log.debug?.("skipping empty message");
47
+ return;
48
+ }
49
+ const policyDecision = evaluateOmadeusInboundPolicy({
50
+ inbound,
51
+ omadeusCfg,
52
+ selfReferenceId
53
+ });
54
+ if (!policyDecision.allow) {
55
+ log.info("omadeus: dropped message by inbound policy", {
56
+ reason: policyDecision.reason,
57
+ ...policyDecision.details ?? {},
58
+ roomId: inbound.roomId,
59
+ kind: inbound.subscribableKind,
60
+ fromReferenceId: inbound.fromReferenceId,
61
+ isMention: inbound.isMention
62
+ });
63
+ return;
64
+ }
65
+ const commandGate = resolveControlCommandGate({
66
+ useAccessGroups: cfg.commands?.useAccessGroups !== false,
67
+ authorizers: [],
68
+ allowTextCommands: true,
69
+ hasControlCommand: core.channel.text.hasControlCommand(rawBody, cfg)
70
+ });
71
+ if (commandGate.shouldBlock) {
72
+ logInboundDrop({
73
+ log: (msg) => log.debug?.(msg),
74
+ channel: "omadeus",
75
+ reason: "control command (unauthorized)",
76
+ target: senderId
77
+ });
78
+ return;
79
+ }
80
+ let bodyForAgent = rawBody;
81
+ const createIntent = parseChannelTaskCreateIntent(rawBody);
82
+ if (createIntent) try {
83
+ const memberReferenceId = inbound.fromReferenceId;
84
+ const created = await createNugget(outboundDeps.apiOpts, {
85
+ title: createIntent.title,
86
+ description: createIntent.description,
87
+ kind: createIntent.kind,
88
+ priority: createIntent.priority,
89
+ stage: "Triage",
90
+ memberReferenceId,
91
+ clientId: 1,
92
+ folderId: 1
93
+ });
94
+ const createdLabel = typeof created["number"] === "number" ? `N${created["number"]}` : String(created["id"] ?? "created");
95
+ const recurring = parseRecurringScheduleIntent(rawBody);
96
+ if (recurring && typeof created["number"] === "number") {
97
+ const cronExpr = recurring.everyMinutes === 60 ? "0 * * * *" : `*/${recurring.everyMinutes} * * * *`;
98
+ const taskRoomId = resolveTaskChannelRoomId(created);
99
+ const taskTarget = taskRoomId ? `room:${taskRoomId}` : `N${created["number"]}`;
100
+ bodyForAgent = `${rawBody}\n\n[Omadeus create] Created ${createIntent.kind} ${createdLabel}.\n[Scheduling required] The user asked for recurring execution.\nYou MUST use the cron tool now (no simulation) to add a job with:\n- schedule.kind: "cron"\n- schedule.expr: "${cronExpr}"\n- payload.kind: "agentTurn"\n- payload.message: "${createIntent.description}"\n- payload.deliver: true\n- payload.channel: "omadeus"\n- payload.to: "${taskTarget}"\n- sessionTarget: "isolated"\n- delivery.mode: "announce"\n- delivery.channel: "omadeus"\n- delivery.to: "${taskTarget}"\nDo NOT deliver to the current selected channel; delivery must go only to the created task private channel target above.\nThen confirm cron job creation to the user.`;
101
+ } else bodyForAgent = `${rawBody}\n\n[Omadeus create] Created ${createIntent.kind} ${createdLabel}.`;
102
+ } catch (err) {
103
+ const errorMessage = err instanceof Error ? err.message : String(err);
104
+ runtime.error?.(`omadeus channel-triggered task create failed: ${errorMessage}`);
105
+ }
106
+ const nuggetIntent = parseNuggetLookupIntent(rawBody);
107
+ if (nuggetIntent) try {
108
+ const nugget = await searchNuggetByNumber(outboundDeps.apiOpts, { nuggetNumber: nuggetIntent.nuggetNumber });
109
+ bodyForAgent = await appendNuggetLookupContextForAgent(rawBody, nuggetIntent.nuggetNumber, nugget, outboundDeps.apiOpts);
110
+ } catch (err) {
111
+ const errorMessage = err instanceof Error ? err.message : String(err);
112
+ runtime.error?.(`omadeus nugget lookup failed: ${errorMessage}`);
113
+ bodyForAgent = await appendNuggetLookupContextForAgent(rawBody, nuggetIntent.nuggetNumber, null, outboundDeps.apiOpts, errorMessage);
114
+ }
115
+ if (!isDirectMessage && (inbound.subscribableKind === "task" || inbound.subscribableKind === "nugget") && !nuggetIntent && !createIntent && !shouldSkipTaskRoomNuggetFetch(rawBody)) try {
116
+ const nugget = await findNuggetByTaskChannelRoom(outboundDeps.apiOpts, {
117
+ roomId: inbound.roomId,
118
+ roomName: inbound.roomName
119
+ });
120
+ bodyForAgent = await appendNuggetContextForTaskOrNuggetRoom(bodyForAgent, inbound.roomId, inbound.roomName, nugget, outboundDeps.apiOpts);
121
+ } catch (err) {
122
+ const errorMessage = err instanceof Error ? err.message : String(err);
123
+ runtime.error?.(`omadeus task room nugget lookup failed: ${errorMessage}`);
124
+ bodyForAgent = await appendNuggetContextForTaskOrNuggetRoom(bodyForAgent, inbound.roomId, inbound.roomName, null, outboundDeps.apiOpts, errorMessage);
125
+ }
126
+ const omadeusFrom = isDirectMessage ? `omadeus:${senderId}` : `omadeus:group:${roomId}`;
127
+ const omadeusTo = isDirectMessage ? `room:${roomId}` : `room:${roomId}`;
128
+ const route = core.channel.routing.resolveAgentRoute({
129
+ cfg,
130
+ channel: "omadeus",
131
+ peer: {
132
+ kind: isDirectMessage ? "direct" : "group",
133
+ id: isDirectMessage ? senderId : roomId
134
+ }
135
+ });
136
+ if (!isDirectMessage && OMADEUS_INBOUND_ENTITY_KIND_SET.has(String(inbound.subscribableKind))) {
137
+ const entityLine = buildOmadeusEntityRoomContextLine(String(inbound.subscribableKind));
138
+ bodyForAgent = `${bodyForAgent}\n\n[OpenClaw] ${entityLine} \`session_status\` is only for **OpenClaw** gateway session state (model/usage, etc.); for that, use this key or \`current\`: ${route.sessionKey} — never \`task/\` + a title as a session key.`;
139
+ }
140
+ const preview = rawBody.replace(/\s+/g, " ").slice(0, 160);
141
+ const inboundLabel = isDirectMessage ? `Omadeus DM from ${senderName}` : `Omadeus message in ${inbound.subscribableKind}/${inbound.roomName ?? roomId} from ${senderName}`;
142
+ core.system.enqueueSystemEvent(`${inboundLabel}: ${preview}`, {
143
+ sessionKey: route.sessionKey,
144
+ contextKey: `omadeus:message:${roomId}:${inbound.timestamp}`
145
+ });
146
+ const envelopeFrom = isDirectMessage ? senderName : inbound.roomName ?? roomId;
147
+ const storePath = core.channel.session.resolveStorePath(cfg.session?.store, { agentId: route.agentId });
148
+ const envelopeOptions = core.channel.reply.resolveEnvelopeFormatOptions(cfg);
149
+ const timestamp = inbound.timestamp ? new Date(inbound.timestamp) : void 0;
150
+ const previousTimestamp = core.channel.session.readSessionUpdatedAt({
151
+ storePath,
152
+ sessionKey: route.sessionKey
153
+ });
154
+ const body = core.channel.reply.formatAgentEnvelope({
155
+ channel: "Omadeus",
156
+ from: envelopeFrom,
157
+ timestamp,
158
+ previousTimestamp,
159
+ envelope: envelopeOptions,
160
+ body: rawBody
161
+ });
162
+ const ctxPayload = core.channel.reply.finalizeInboundContext({
163
+ Body: body,
164
+ BodyForAgent: bodyForAgent,
165
+ RawBody: rawBody,
166
+ CommandBody: rawBody.trim(),
167
+ BodyForCommands: rawBody.trim(),
168
+ /** Lets the message tool default `react` / `edit` to this Jaguar message id. */
169
+ MessageSid: String(inbound.messageId),
170
+ From: omadeusFrom,
171
+ To: omadeusTo,
172
+ SessionKey: route.sessionKey,
173
+ AccountId: route.accountId,
174
+ ChatType: isDirectMessage ? "direct" : "group",
175
+ ConversationLabel: envelopeFrom,
176
+ GroupSubject: !isDirectMessage ? inbound.roomName ?? inbound.subscribableKind : void 0,
177
+ SenderName: senderName,
178
+ SenderId: senderId,
179
+ Provider: "omadeus",
180
+ Surface: "omadeus",
181
+ Timestamp: inbound.timestamp ?? Date.now(),
182
+ WasMentioned: isDirectMessage || inbound.isMention,
183
+ CommandAuthorized: commandGate.commandAuthorized,
184
+ OriginatingChannel: "omadeus",
185
+ OriginatingTo: omadeusTo
186
+ });
187
+ await core.channel.session.recordInboundSession({
188
+ storePath,
189
+ sessionKey: ctxPayload.SessionKey ?? route.sessionKey,
190
+ ctx: ctxPayload,
191
+ onRecordError: (err) => {
192
+ log.debug?.(`omadeus: failed updating session meta: ${String(err)}`);
193
+ }
194
+ });
195
+ const { dispatcher, replyOptions, markDispatchIdle } = createOmadeusReplyDispatcher({
196
+ cfg,
197
+ agentId: route.agentId,
198
+ accountId: route.accountId,
199
+ runtime,
200
+ log,
201
+ outboundDeps,
202
+ roomId
203
+ });
204
+ log.info("dispatching to agent", { sessionKey: route.sessionKey });
205
+ try {
206
+ const { queuedFinal, counts } = await core.channel.reply.withReplyDispatcher({
207
+ dispatcher,
208
+ onSettled: () => {
209
+ markDispatchIdle();
210
+ },
211
+ run: () => core.channel.reply.dispatchReplyFromConfig({
212
+ ctx: ctxPayload,
213
+ cfg,
214
+ dispatcher,
215
+ replyOptions
216
+ })
217
+ });
218
+ log.info("dispatch complete", {
219
+ queuedFinal,
220
+ counts
221
+ });
222
+ const finalCount = counts.final;
223
+ if (queuedFinal) log.debug?.(`omadeus: delivered ${finalCount} repl${finalCount === 1 ? "y" : "ies"} to room ${roomId}`);
224
+ } catch (err) {
225
+ log.error("dispatch failed", { error: String(err) });
226
+ runtime.error?.(`omadeus dispatch failed: ${String(err)}`);
227
+ }
228
+ };
229
+ const inboundDebouncer = core.channel.debounce.createInboundDebouncer({
230
+ debounceMs: inboundDebounceMs,
231
+ buildKey: (entry) => {
232
+ return `omadeus:${entry.roomId}:${entry.fromReferenceId}`;
233
+ },
234
+ shouldDebounce: (entry) => {
235
+ if (!entry.content.trim()) return false;
236
+ return !core.channel.text.hasControlCommand(entry.content, cfg);
237
+ },
238
+ onFlush: async (entries) => {
239
+ const last = entries.at(-1);
240
+ if (!last) return;
241
+ if (entries.length === 1) {
242
+ await handleMessageNow(last);
243
+ return;
244
+ }
245
+ const combinedContent = entries.map((e) => e.content).filter(Boolean).join("\n");
246
+ if (!combinedContent.trim()) return;
247
+ await handleMessageNow({
248
+ ...last,
249
+ content: combinedContent,
250
+ isMention: entries.some((e) => e.isMention)
251
+ });
252
+ },
253
+ onError: (err) => {
254
+ runtime.error?.(`omadeus debounce flush failed: ${String(err)}`);
255
+ }
256
+ });
257
+ return async function handleOmadeusMessage(inbound) {
258
+ await inboundDebouncer.enqueue(inbound);
259
+ };
260
+ }
261
+ //#endregion
262
+ export { createOmadeusMessageHandler };