MCPcopy
hub / github.com/coder/mux / sendMessage

Method sendMessage

src/node/services/agentSession.ts:2285–2901  ·  view source on GitHub ↗
(
    message: string,
    options?: SendMessageOptions & { fileParts?: FilePart[] },
    internal?: {
      synthetic?: boolean;
      agentInitiated?: boolean;
      goalContinuation?: boolean;
      goalKind?: GoalSyntheticMessageKind;
      startStreamInBackground?: boolean;
      onAccepted?: () => Promise<void> | void;
      onAcceptedPreStreamFailure?: (error: SendMessageError) => Promise<void> | void;
      onCanceled?: (reason: string) => Promise<void> | void;
    }
  )

Source from the content-addressed store, hash-verified

2283 }
2284
2285 async sendMessage(
2286 message: string,
2287 options?: SendMessageOptions & { fileParts?: FilePart[] },
2288 internal?: {
2289 synthetic?: boolean;
2290 agentInitiated?: boolean;
2291 goalContinuation?: boolean;
2292 goalKind?: GoalSyntheticMessageKind;
2293 startStreamInBackground?: boolean;
2294 onAccepted?: () => Promise<void> | void;
2295 onAcceptedPreStreamFailure?: (error: SendMessageError) => Promise<void> | void;
2296 onCanceled?: (reason: string) => Promise<void> | void;
2297 }
2298 ): Promise<Result<void, SendMessageError>> {
2299 this.assertNotDisposed("sendMessage");
2300
2301 assert(typeof message === "string", "sendMessage requires a string message");
2302
2303 const isManualUserMessage = internal?.synthetic !== true;
2304
2305 // Last-line-of-defence pricing gate: every dispatch path (initial sends,
2306 // sendQueuedMessages, dispatchPendingFollowUp,
2307 // post-compaction follow-ups) lands here, so a budgeted goal that became
2308 // resumable while a queued unpriced-model message waited cannot bypass
2309 // enforcement. The WorkspaceService-level gate already runs first for
2310 // initial calls (and prevents persisting bad AI settings), but it cannot
2311 // catch goal-state changes that happen between queueing and dispatch.
2312 //
2313 // When rejecting a manual (user-typed) send, we MUST persist the user's
2314 // message and surface a stream-error chat event before returning. The
2315 // queue-dispatch flow in `sendQueuedMessages()` removes the message from
2316 // the queue before calling us, so a silent `Err` here would drop the
2317 // user's input without any visible feedback (Codex P1
2318 // PRRT_kwDOPxxmWM5_s-jo). For synthetic sends (compaction, goal
2319 // continuation, etc.) the user did not type the message, so we just
2320 // return Err and let the synthetic caller log/handle it.
2321 if (this.workspaceGoalService) {
2322 const pricingGate = await this.workspaceGoalService.assertPricedModelForBudgetedGoal(
2323 this.workspaceId,
2324 options?.model
2325 );
2326 if (!pricingGate.success) {
2327 if (isManualUserMessage) {
2328 const persisted = await this.preserveRejectedManualSend(
2329 message,
2330 options,
2331 pricingGate.error
2332 );
2333 // The user has explicitly intervened, so the goal-safety contract
2334 // for manual sends must still apply on the rejection path: clear any
2335 // pending acknowledgment gate AND auto-pause an active goal so a
2336 // pending post-stream-end continuation does not fire as if the user
2337 // had not interrupted (Codex P1 PRRT_kwDOPxxmWM5_tOFt). Only run the
2338 // hook when an actionable manual turn was actually present — empty
2339 // payloads (Codex P2 PRRT_kwDOPxxmWM5_tUsx) would otherwise silently
2340 // disable goal continuation after a blank submit / invalid payload.
2341 if (persisted) {
2342 await this.applyManualUserMessageGoalSafety({ policy: "pause" });

Callers 3

sendQueuedMessagesMethod · 0.95

Calls 15

assertNotDisposedMethod · 0.95
isBusyMethod · 0.95
interruptStreamMethod · 0.95
setTurnPhaseMethod · 0.95
waitForIdleMethod · 0.95
restoreQueueToInputMethod · 0.95

Tested by

no test coverage detected