{"record":{"id":"7982ea1b4f5ac2ad","repo":"tinyhumansai/openhuman","slug":"subagentapi-cancel-taskid-is-required","errorCode":null,"errorMessage":"subagentApi.cancel: taskId is required","messagePattern":"subagentApi\\.cancel: taskId is required","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"warning","filePath":"app/src/services/api/subagentApi.ts","lineNumber":31,"sourceCode":"\nconst log = debug('subagentApi');\n\n/** Result of a cancel request. Mirrors the Rust handler payload. */\ninterface SubagentCancelResult {\n  /** True if a running sub-agent was aborted; false if it was already done/unknown. */\n  cancelled: boolean;\n  taskId: string;\n}\n\nexport const subagentApi = {\n  /**\n   * Cancel a still-running detached background sub-agent by its spawn task id.\n   * Resolves with `cancelled: false` (not an error) when the sub-agent already\n   * finished or the id is unknown.\n   */\n  cancel: async (taskId: string, reason?: string): Promise<SubagentCancelResult> => {\n    const id = taskId.trim();\n    if (!id) throw new Error('subagentApi.cancel: taskId is required');\n    const params: Record<string, unknown> = { taskId: id };\n    const trimmedReason = reason?.trim();\n    if (trimmedReason) params.reason = trimmedReason;\n    log('cancel taskId=%s', id);\n    const result = await callCoreRpc<SubagentCancelResult>({\n      method: 'openhuman.subagent_cancel',\n      params,\n    });\n    log('cancel received cancelled=%s', result.cancelled);\n    return result;\n  },\n};\n","sourceCodeStart":13,"sourceCodeEnd":44,"githubUrl":"https://github.com/tinyhumansai/openhuman/blob/a221052e0df5b1f7598fceba7329fd1af95d6699/app/src/services/api/subagentApi.ts#L13-L44","documentation":"A client-side precondition guard in subagentApi.cancel(): it trims the taskId argument and throws before any RPC if the result is empty. The underlying 'openhuman.subagent_cancel' RPC needs a spawn task id; the guard turns an obvious programming mistake into an immediate, descriptive error instead of a pointless core round-trip.","triggerScenarios":"Calling subagentApi.cancel(''), cancel('   '), or cancel(someUndefined) (undefined.trim() would throw earlier — the usual case is an empty string). Typically a UI cancel button wired to a row whose taskId never got populated because the spawn response lacked it.","commonSituations":"A background-work list renders rows from a payload where taskId is optional, and the cancel handler assumes it is always present; a 'cancel all' loop passes a default '' placeholder; refactoring changed the row model and taskId moved/nested.","solutions":["Guard at the call site: skip (or no-op) when the trimmed id is empty — there is nothing to cancel","Only render/enable the cancel control when the row actually has a non-empty taskId","Fix the upstream shape: if the spawn result is missing taskId, that is the real bug — inspect what produced the row data","In tests, pass a real id like 'task-123' instead of ''"],"exampleFix":"// before\nonCancel={() => subagentApi.cancel(row.taskId)}\n\n// after\nonCancel={() => {\n  const id = row.taskId?.trim();\n  if (!id) return; // nothing to cancel\n  subagentApi.cancel(id);\n}}","handlingStrategy":"validation","validationCode":"const id = taskId?.trim();\nif (!id) {\n  // Nothing to cancel — treat as the API's own 'already finished' semantics\n  return;\n}\nawait subagentApi.cancel(id, reason);","typeGuard":null,"tryCatchPattern":null,"preventionTips":["Only render cancel controls for rows with a non-empty taskId","Type row models so taskId is present where cancel is offered","Treat this throw as a lint-level bug: it should never fire in production code"],"tags":["validation","precondition","subagent","ui"],"backgroundTag":null,"analyzedSha":"a221052e0df5b1f7598fceba7329fd1af95d6699","analyzedAt":"2026-08-16T12:47:06.542Z","schemaVersion":2},"datasetVersion":"2026-08-16T13:17:31.715Z"}