@femtomc/mu-server 26.2.97 → 26.2.99

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 CHANGED
@@ -1,8 +1,12 @@
1
1
  # @femtomc/mu-server
2
2
 
3
- HTTP API server for mu control-plane infrastructure. Powers `mu serve`, messaging frontend transport routes, and control-plane/session coordination endpoints.
3
+ HTTP API server for mu control-plane infrastructure.
4
+ Powers `mu serve`, messaging frontend transport routes, and
5
+ control-plane/session coordination endpoints.
4
6
 
5
- > Scope note: server-routed business query/mutation gateway endpoints have been removed. Business reads/writes are CLI-first, while long-lived runtime coordination (runs/heartbeats/cron) stays server-owned.
7
+ > Scope note: server-routed business query/mutation gateway endpoints have
8
+ > been removed. Business reads/writes are CLI-first, while long-lived runtime
9
+ > coordination (runs/heartbeats/cron) stays server-owned.
6
10
 
7
11
  ## Installation
8
12
 
@@ -141,66 +145,45 @@ Use `mu store paths --pretty` to resolve `<store>` for the active repo/workspace
141
145
  - `GET /api/control-plane/events`
142
146
  - `GET /api/control-plane/events/tail`
143
147
 
144
- ## Messaging adapter setup (workspace config)
148
+ ## Messaging adapter setup (skills-first)
145
149
 
146
- 1) Resolve workspace paths and inspect current readiness:
150
+ For first-time channel onboarding, prefer bundled setup skills from `mu`
151
+ (`setup-slack`, `setup-discord`, `setup-telegram`, `setup-neovim`).
152
+ These workflows are agent-first: the agent patches config, reloads control-plane,
153
+ verifies routes/capabilities, collects IDs from audit where possible, and asks users
154
+ only for required external-console steps and secret handoff.
147
155
 
148
- ```bash
149
- mu store paths --pretty
150
- mu control status --pretty
151
- ```
152
-
153
- 2) Edit `<store>/config.json` and set adapter secrets:
154
-
155
- - Slack: `control_plane.adapters.slack.signing_secret`, `bot_token`
156
- - Discord: `control_plane.adapters.discord.signing_secret`
157
- - Telegram: `control_plane.adapters.telegram.webhook_secret`, `bot_token`, `bot_username`
158
- - Neovim: `control_plane.adapters.neovim.shared_secret`
159
- - Optional operator tuning: `control_plane.operator.timeout_ms` (max wall-time per messaging turn, default `600000`).
160
-
161
- 3) Reload live control-plane runtime:
156
+ Baseline status/verification commands:
162
157
 
163
158
  ```bash
159
+ mu control status --pretty
160
+ mu store paths --pretty
164
161
  mu control reload
165
- # or POST /api/control-plane/reload
162
+ curl -s http://localhost:3000/api/control-plane/channels | jq '.channels'
163
+ mu control identities --all --pretty
166
164
  ```
167
165
 
168
- 4) Link identities for channel actors (examples):
169
-
170
- ```bash
171
- mu control link --channel slack --actor-id U123 --tenant-id T123
172
- mu control link --channel discord --actor-id <user-id> --tenant-id <guild-id>
173
- mu control link --channel telegram --actor-id <chat-id> --tenant-id telegram-bot
174
- ```
175
-
176
- For Neovim, use `:Mu link` in `mu.nvim` after configuring `shared_secret`.
177
-
178
166
  ## Media support operations checklist
179
167
 
180
- When validating attachment support end-to-end, use this sequence:
168
+ When validating attachment support end-to-end:
181
169
 
182
- 1. Configure adapter secrets/tokens in `<store>/config.json` (Slack/Telegram need bot tokens for media egress).
170
+ 1. Ensure Slack/Telegram bot tokens are configured in `<store>/config.json`.
183
171
  2. Reload control-plane (`mu control reload`).
184
- 3. Verify `/api/control-plane/channels` media capability flags:
172
+ 3. Verify `/api/control-plane/channels` media flags:
185
173
  - `media.outbound_delivery`
186
174
  - `media.inbound_attachment_download`
187
- 4. Send a text-only control-plane turn and verify normal delivery semantics still hold.
188
- 5. Send attachment-bearing ingress/outbox payloads and verify channel-specific routing:
189
- - Slack media upload through `files.upload`
190
- - Telegram PNG/JPEG/WEBP images through `sendPhoto`
191
- - Telegram SVG/PDF through `sendDocument`
192
-
193
- Operational fallback expectations:
194
-
195
- - If media upload fails, Telegram delivery falls back to text `sendMessage`.
196
- - Telegram text delivery chunks long messages into deterministic in-order `sendMessage` calls to stay below API size limits.
197
- - When outbound metadata includes `telegram_reply_to_message_id`, Telegram delivery anchors replies to the originating chat message.
198
- - Invalid/non-integer `telegram_reply_to_message_id` metadata is ignored so delivery degrades gracefully to non-anchored sends.
199
- - Awaiting-confirmation envelopes include Telegram inline `Confirm`/`Cancel` callback buttons when interaction metadata provides confirmation actions.
200
- - Telegram callback payloads are contract-limited to `confirm:<command_id>` and `cancel:<command_id>`; unsupported payloads are explicitly rejected.
201
- - Callback buttons keep parity with command fallback: `/mu confirm <id>` and `/mu cancel <id>` remain valid.
202
- - Group/supergroup Telegram freeform text is deterministic no-op with guidance; explicit `/mu ...` commands stay actionable.
203
- - If Slack/Telegram bot token is missing, channel capability reason codes should report `*_bot_token_missing` and outbound delivery retries rather than hard-crashing runtime.
175
+ 4. Run one text-only turn and verify normal delivery.
176
+ 5. Run one attachment-bearing turn and verify channel-specific routing:
177
+ - Slack media upload via `files.upload`
178
+ - Telegram PNG/JPEG/WEBP via `sendPhoto`
179
+ - Telegram SVG/PDF via `sendDocument`
180
+
181
+ Operational fallbacks:
182
+
183
+ - Telegram media upload failure falls back to text `sendMessage`.
184
+ - Telegram long text is deterministically chunked into ordered `sendMessage` calls.
185
+ - `telegram_reply_to_message_id` metadata anchors replies when parseable.
186
+ - Missing Slack/Telegram bot tokens surface capability reason codes (`*_bot_token_missing`) and retry behavior.
204
187
 
205
188
  ## Running the Server
206
189
 
@@ -161,52 +161,8 @@ export function splitSlackMessageText(text, maxLen = SLACK_MESSAGE_MAX_LEN) {
161
161
  }
162
162
  return chunks;
163
163
  }
164
- function slackBlocksForOutboxRecord(record, body) {
165
- const interactionMessage = record.envelope.metadata?.interaction_message;
166
- if (!interactionMessage || typeof interactionMessage !== "object") {
167
- return undefined;
168
- }
169
- const rawActions = interactionMessage.actions;
170
- if (!Array.isArray(rawActions) || rawActions.length === 0) {
171
- return undefined;
172
- }
173
- const buttons = [];
174
- for (const action of rawActions) {
175
- if (!action || typeof action !== "object") {
176
- continue;
177
- }
178
- const candidate = action;
179
- if (typeof candidate.label !== "string" || typeof candidate.command !== "string") {
180
- continue;
181
- }
182
- const normalized = candidate.command.trim().replace(/^\/mu\s+/i, "");
183
- const confirm = /^confirm\s+([^\s]+)$/i.exec(normalized);
184
- if (confirm?.[1]) {
185
- buttons.push({
186
- type: "button",
187
- text: { type: "plain_text", text: candidate.label },
188
- value: `confirm:${confirm[1]}`,
189
- action_id: `confirm_${confirm[1]}`,
190
- });
191
- continue;
192
- }
193
- const cancel = /^cancel\s+([^\s]+)$/i.exec(normalized);
194
- if (cancel?.[1]) {
195
- buttons.push({
196
- type: "button",
197
- text: { type: "plain_text", text: candidate.label },
198
- value: `cancel:${cancel[1]}`,
199
- action_id: `cancel_${cancel[1]}`,
200
- });
201
- }
202
- }
203
- if (buttons.length === 0) {
204
- return undefined;
205
- }
206
- return [
207
- { type: "section", text: { type: "mrkdwn", text: body } },
208
- { type: "actions", elements: buttons },
209
- ];
164
+ function slackBlocksForOutboxRecord(_record, _body) {
165
+ return undefined;
210
166
  }
211
167
  function slackThreadTsFromMetadata(metadata) {
212
168
  const candidates = [metadata?.slack_thread_ts, metadata?.slack_message_ts, metadata?.thread_ts];
@@ -225,36 +181,8 @@ function slackStatusMessageTsFromMetadata(metadata) {
225
181
  const trimmed = value.trim();
226
182
  return trimmed.length > 0 ? trimmed : undefined;
227
183
  }
228
- function telegramReplyMarkupForOutboxRecord(record) {
229
- const interactionMessage = record.envelope.metadata?.interaction_message;
230
- if (!interactionMessage || typeof interactionMessage !== "object") {
231
- return undefined;
232
- }
233
- const rawActions = interactionMessage.actions;
234
- if (!Array.isArray(rawActions) || rawActions.length === 0) {
235
- return undefined;
236
- }
237
- const row = [];
238
- for (const action of rawActions) {
239
- if (!action || typeof action !== "object") {
240
- continue;
241
- }
242
- const candidate = action;
243
- if (typeof candidate.label !== "string" || typeof candidate.command !== "string") {
244
- continue;
245
- }
246
- const normalized = candidate.command.trim().replace(/^\/mu\s+/i, "");
247
- const confirm = /^confirm\s+([^\s]+)$/i.exec(normalized);
248
- if (confirm?.[1]) {
249
- row.push({ text: candidate.label, callback_data: `confirm:${confirm[1]}` });
250
- continue;
251
- }
252
- const cancel = /^cancel\s+([^\s]+)$/i.exec(normalized);
253
- if (cancel?.[1]) {
254
- row.push({ text: candidate.label, callback_data: `cancel:${cancel[1]}` });
255
- }
256
- }
257
- return row.length > 0 ? { inline_keyboard: [row] } : undefined;
184
+ function telegramReplyMarkupForOutboxRecord(_record) {
185
+ return undefined;
258
186
  }
259
187
  async function postTelegramMessage(botToken, payload) {
260
188
  return await fetch(`https://api.telegram.org/bot${botToken}/sendMessage`, {
@@ -710,94 +638,6 @@ export async function bootstrapControlPlane(opts) {
710
638
  pipeline = new ControlPlaneCommandPipeline({
711
639
  runtime,
712
640
  operator,
713
- mutationExecutor: async (record) => {
714
- if (record.target_type === "reload" || record.target_type === "update") {
715
- if (record.command_args.length > 0) {
716
- return {
717
- terminalState: "failed",
718
- errorCode: "cli_validation_failed",
719
- trace: {
720
- cliCommandKind: record.target_type,
721
- },
722
- mutatingEvents: [
723
- {
724
- eventType: "session.lifecycle.command.failed",
725
- payload: {
726
- action: record.target_type,
727
- reason: "unexpected_args",
728
- args: record.command_args,
729
- },
730
- },
731
- ],
732
- };
733
- }
734
- const action = record.target_type;
735
- const executeLifecycleAction = action === "reload" ? opts.sessionLifecycle.reload : opts.sessionLifecycle.update;
736
- try {
737
- const lifecycle = await executeLifecycleAction();
738
- if (!lifecycle.ok) {
739
- return {
740
- terminalState: "failed",
741
- errorCode: "session_lifecycle_failed",
742
- trace: {
743
- cliCommandKind: action,
744
- },
745
- mutatingEvents: [
746
- {
747
- eventType: "session.lifecycle.command.failed",
748
- payload: {
749
- action,
750
- reason: lifecycle.message,
751
- details: lifecycle.details ?? null,
752
- },
753
- },
754
- ],
755
- };
756
- }
757
- return {
758
- terminalState: "completed",
759
- result: {
760
- ok: true,
761
- action,
762
- message: lifecycle.message,
763
- details: lifecycle.details ?? null,
764
- },
765
- trace: {
766
- cliCommandKind: action,
767
- },
768
- mutatingEvents: [
769
- {
770
- eventType: `session.lifecycle.command.${action}`,
771
- payload: {
772
- action,
773
- message: lifecycle.message,
774
- details: lifecycle.details ?? null,
775
- },
776
- },
777
- ],
778
- };
779
- }
780
- catch (err) {
781
- return {
782
- terminalState: "failed",
783
- errorCode: err instanceof Error && err.message ? err.message : "session_lifecycle_failed",
784
- trace: {
785
- cliCommandKind: action,
786
- },
787
- mutatingEvents: [
788
- {
789
- eventType: "session.lifecycle.command.failed",
790
- payload: {
791
- action,
792
- reason: err instanceof Error && err.message ? err.message : "session_lifecycle_failed",
793
- },
794
- },
795
- ],
796
- };
797
- }
798
- }
799
- return null;
800
- },
801
641
  });
802
642
  await pipeline.start();
803
643
  const telegramManager = new TelegramAdapterGenerationManager({
@@ -1058,11 +898,11 @@ export async function bootstrapControlPlane(opts) {
1058
898
  }
1059
899
  return result;
1060
900
  },
1061
- async submitTerminalCommand(terminalOpts) {
901
+ async submitAutonomousIngress(autonomousOpts) {
1062
902
  if (!pipeline) {
1063
903
  throw new Error("control_plane_pipeline_unavailable");
1064
904
  }
1065
- return await pipeline.handleTerminalInbound(terminalOpts);
905
+ return await pipeline.handleAutonomousIngress(autonomousOpts);
1066
906
  },
1067
907
  async stop() {
1068
908
  wakeDeliveryObserver = null;
@@ -123,10 +123,11 @@ export type ControlPlaneHandle = {
123
123
  config: ControlPlaneConfig;
124
124
  reason: string;
125
125
  }): Promise<TelegramGenerationReloadResult>;
126
- submitTerminalCommand?(opts: {
127
- commandText: string;
126
+ submitAutonomousIngress?(opts: {
127
+ text: string;
128
128
  repoRoot: string;
129
129
  requestId?: string;
130
+ metadata?: Record<string, unknown>;
130
131
  }): Promise<CommandPipelineResult>;
131
132
  stop(): Promise<void>;
132
133
  };
package/dist/server.js CHANGED
@@ -74,7 +74,7 @@ function extractWakeTurnReply(turnResult) {
74
74
  const compact = presented.compact.trim();
75
75
  return compact.length > 0 ? compact : null;
76
76
  }
77
- function buildWakeTurnCommandText(opts) {
77
+ function buildWakeTurnIngressText(opts) {
78
78
  const wakeSource = stringField(opts.payload, "wake_source") ?? "unknown";
79
79
  const programId = stringField(opts.payload, "program_id") ?? "unknown";
80
80
  const title = stringField(opts.payload, "title") ?? "(untitled wake program)";
@@ -98,7 +98,7 @@ function buildWakeTurnCommandText(opts) {
98
98
  "",
99
99
  `payload=${payloadSnapshot}`,
100
100
  "",
101
- "If action is needed, produce exactly one `/mu ...` command. If no action is needed, return a short operator response that can be broadcast verbatim.",
101
+ "Respond conversationally with exactly one concise operator message suitable for immediate broadcast.",
102
102
  ].join("\n");
103
103
  }
104
104
  export function createContext(repoRoot) {
@@ -142,7 +142,8 @@ function createServer(options = {}) {
142
142
  const programId = stringField(opts.payload, "program_id");
143
143
  const sourceTsMs = numberField(opts.payload, "source_ts_ms");
144
144
  let decision;
145
- if (typeof controlPlaneProxy.submitTerminalCommand !== "function") {
145
+ const autonomousIngress = controlPlaneProxy.submitAutonomousIngress;
146
+ if (typeof autonomousIngress !== "function") {
146
147
  decision = {
147
148
  outcome: "fallback",
148
149
  reason: "control_plane_unavailable",
@@ -155,14 +156,21 @@ function createServer(options = {}) {
155
156
  else {
156
157
  const turnRequestId = `wake-turn-${wakeId}`;
157
158
  try {
158
- const turnResult = await controlPlaneProxy.submitTerminalCommand({
159
- commandText: buildWakeTurnCommandText({
160
- wakeId,
161
- message: opts.message,
162
- payload: opts.payload,
163
- }),
159
+ const ingressText = buildWakeTurnIngressText({
160
+ wakeId,
161
+ message: opts.message,
162
+ payload: opts.payload,
163
+ });
164
+ const turnResult = await autonomousIngress({
165
+ text: ingressText,
164
166
  repoRoot: context.repoRoot,
165
167
  requestId: turnRequestId,
168
+ metadata: {
169
+ wake_id: wakeId,
170
+ wake_source: wakeSource,
171
+ program_id: programId,
172
+ source_ts_ms: sourceTsMs,
173
+ },
166
174
  });
167
175
  if (turnResult.kind === "noop" || turnResult.kind === "invalid") {
168
176
  decision = {
@@ -404,12 +412,12 @@ function createServer(options = {}) {
404
412
  const handle = reloadManager.getControlPlaneCurrent();
405
413
  handle?.setWakeDeliveryObserver?.(observer ?? null);
406
414
  },
407
- async submitTerminalCommand(opts) {
415
+ async submitAutonomousIngress(opts) {
408
416
  const handle = reloadManager.getControlPlaneCurrent();
409
- if (!handle?.submitTerminalCommand) {
410
- throw new Error("control_plane_unavailable");
417
+ if (handle?.submitAutonomousIngress) {
418
+ return await handle.submitAutonomousIngress(opts);
411
419
  }
412
- return await handle.submitTerminalCommand(opts);
420
+ throw new Error("control_plane_unavailable");
413
421
  },
414
422
  async stop() {
415
423
  const handle = reloadManager.getControlPlaneCurrent();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@femtomc/mu-server",
3
- "version": "26.2.97",
3
+ "version": "26.2.99",
4
4
  "description": "HTTP API server for mu control-plane transport/session plus run/activity scheduling coordination.",
5
5
  "keywords": [
6
6
  "mu",
@@ -30,8 +30,8 @@
30
30
  "start": "bun run dist/cli.js"
31
31
  },
32
32
  "dependencies": {
33
- "@femtomc/mu-agent": "26.2.97",
34
- "@femtomc/mu-control-plane": "26.2.97",
35
- "@femtomc/mu-core": "26.2.97"
33
+ "@femtomc/mu-agent": "26.2.99",
34
+ "@femtomc/mu-control-plane": "26.2.99",
35
+ "@femtomc/mu-core": "26.2.99"
36
36
  }
37
37
  }