{"record":{"id":"5b1ac4ee56f1b000","repo":"paperclipai/paperclip","slug":"capability-live-messages-cannot-be-empty","errorCode":null,"errorMessage":"Capability live messages cannot be empty","messagePattern":"Capability live messages cannot be empty","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"packages/paperclip-runner/src/live/live-session.ts","lineNumber":1461,"sourceCode":"    ) {\n      throw new Error(\"capability_live_attempt_active_turn\");\n    }\n    attempt.status = status;\n    attempt.finishedAt = this.#now().toISOString();\n    attempt.failureCode = status === \"failed\"\n      ? requireNonEmpty(failureCode ?? \"attempt_failed\", \"attempt_failure_code\")\n      : null;\n    await this.#persist();\n    return this.snapshot();\n  }\n\n  async sendMessage(\n    message: string,\n    /** Launch-only diagnostics may opt out; qualification campaigns must not. */\n    options: { allowMissingUsage?: boolean } = {},\n  ): Promise<CapabilityLiveTurnResult> {\n    const value = message.trim();\n    if (value.length === 0) throw new Error(\"Capability live messages cannot be empty\");\n    if (this.#status === \"suspended\" || this.#transport === null) {\n      await this.#connect(true);\n    }\n    if (this.#transport === null || this.#status === \"closed\" || this.#status === \"failed\") {\n      throw new Error(\"Capability live session is not connected\");\n    }\n    if (\n      this.#activeTurnId !== null ||\n      this.#turnWaiter !== null ||\n      this.#pendingTurnAdmission !== null\n    ) {\n      throw new Error(\"Capability live session already has an active turn\");\n    }\n    let settleAdmission!: () => void;\n    const admission: PendingTurnAdmission = {\n      transport: this.#transport,\n      cancellation: null,\n      settled: new Promise<void>((resolveSettled) => {","sourceCodeStart":1443,"sourceCodeEnd":1479,"githubUrl":"https://github.com/paperclipai/paperclip/blob/01ad8584922b5d85292b1723cae71fa0d9b07a19/packages/paperclip-runner/src/live/live-session.ts#L1443-L1479","documentation":"CapabilityLiveSession.sendMessage rejects empty messages. The incoming string is trimmed and if the result has length 0 the method throws before any transport work is done, because an empty user turn would create a meaningless Codex turn.","triggerScenarios":"Calling sendMessage('') or sendMessage('   \\n') — any string that is empty after trimming.","commonSituations":"UI sending an empty input box value; a pipeline forwarding an optional message field that is undefined coerced to empty string; templated prompts that resolve to whitespace only.","solutions":["Provide a non-empty message string","Trim/validate user input before calling sendMessage and skip the call when empty","Check whether an upstream field defaults to '' and supply a real prompt"],"exampleFix":"// before\nawait session.sendMessage(userInput); // '' or whitespace throws\n// after\nconst trimmed = (userInput ?? '').trim();\nif (trimmed.length > 0) await session.sendMessage(trimmed);","handlingStrategy":"validation","validationCode":"const trimmed = (message ?? '').trim();\nif (trimmed.length === 0) throw new Error('refusing to send empty live message');","typeGuard":"function isSendableMessage(m: unknown): m is string {\n  return typeof m === 'string' && m.trim().length > 0;\n}","tryCatchPattern":"try {\n  await session.sendMessage(message);\n} catch (e) {\n  if ((e as Error).message === 'Capability live messages cannot be empty') {\n    // skip turn; fix input source\n  } else throw e;\n}","preventionTips":["Validate/trim chat input in the UI before dispatching","Never pass optional message fields straight through; default and check them","Add a unit assertion that prompt templates render to non-empty text"],"tags":["validation","empty-input"],"backgroundTag":"empty-required-field","analyzedSha":"01ad8584922b5d85292b1723cae71fa0d9b07a19","analyzedAt":"2026-09-10T03:14:50.855Z","contentChangedAt":"2026-09-10T03:14:50.855Z","schemaVersion":2},"datasetVersion":"2026-09-14T05:17:10.506Z"}