* Process a message and return a response * Uses database-backed rules for the system prompt when available * * @param userMessage - The user's message * @param threadContext - Optional thread history * @param requestTools - Optional per-request tools (e.g., user-scoped member tools)
(
userMessage: string,
threadContext?: Array<{ user: string; text: string }>,
requestTools?: RequestTools,
rulesOverride?: RulesOverride,
options?: ProcessMessageOptions
)
| 495 | * @param options - Optional processing options (e.g., maxIterations for admin users) |
| 496 | */ |
| 497 | async processMessage( |
| 498 | userMessage: string, |
| 499 | threadContext?: Array<{ user: string; text: string }>, |
| 500 | requestTools?: RequestTools, |
| 501 | rulesOverride?: RulesOverride, |
| 502 | options?: ProcessMessageOptions |
| 503 | ): Promise<AddieResponse> { |
| 504 | // #2950: warn when a caller has neither `costScope` nor explicit |
| 505 | // `uncapped: true`. Silent default meant a future user-facing |
| 506 | // caller could ship uncapped and nobody would notice — this log |
| 507 | // turns that into an observability signal. A hard throw would |
| 508 | // break legitimate callers we haven't migrated yet; loud-log |
| 509 | // lets audit rules alert on the event. |
| 510 | if (!options?.costScope && !options?.uncapped) { |
| 511 | logger.warn( |
| 512 | { event: 'cost_cap_unwired', method: 'processMessage' }, |
| 513 | 'claude-client called without costScope or uncapped:true — cost cap silently bypassed', |
| 514 | ); |
| 515 | } |
| 516 | |
| 517 | // #2790: per-user Anthropic cost cap. Check at entry; when the |
| 518 | // user has exhausted their daily budget, return a friendly |
| 519 | // "try again later" response instead of firing another |
| 520 | // (billable) Claude call. The caller's ProcessMessageOptions |
| 521 | // carries both `userId` and `tier` so we don't have to resolve |
| 522 | // the subscription tier here. |
| 523 | if (options?.costScope) { |
| 524 | const capResult = await checkCostCap( |
| 525 | options.costScope.userId, |
| 526 | options.costScope.tier, |
| 527 | ); |
| 528 | if (!capResult.ok) { |
| 529 | const message = formatCapExceededMessage(capResult); |
| 530 | logger.warn( |
| 531 | { |
| 532 | userId: options.costScope.userId, |
| 533 | tier: options.costScope.tier, |
| 534 | spentCents: capResult.spentCents, |
| 535 | retryAfterMs: capResult.retryAfterMs, |
| 536 | }, |
| 537 | 'Addie cost cap exceeded — refusing Claude call', |
| 538 | ); |
| 539 | return { |
| 540 | text: message, |
| 541 | tools_used: [], |
| 542 | tool_executions: [], |
| 543 | flagged: true, |
| 544 | flag_reason: 'cost_cap_exceeded', |
| 545 | }; |
| 546 | } |
| 547 | } |
| 548 | |
| 549 | const toolsUsed: string[] = []; |
| 550 | const toolExecutions: ToolExecution[] = []; |
| 551 | let executionSequence = 0; |
| 552 | |
| 553 | // Timing metrics |
| 554 | const timingStart = Date.now(); |
no test coverage detected