{"record":{"id":"1311f8bf104b58e5","repo":"paperclipai/paperclip","slug":"company-name-cannot-be-empty","errorCode":null,"errorMessage":"--company-name cannot be empty.","messagePattern":"--company-name cannot be empty\\.","errorType":"validation","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"cli/src/commands/test-drive.ts","lineNumber":239,"sourceCode":"}\n\nexport function resolveTestDriveBootstrap(\n  options: TestDriveOptions,\n  env: NodeJS.ProcessEnv = process.env,\n): ResolvedTestDriveBootstrap {\n  if (options.apiKey !== undefined && options.apiKeyEnv !== undefined) {\n    throw new Error(\"--api-key and --api-key-env are mutually exclusive.\");\n  }\n\n  const harness = options.harness ?? \"claude\";\n  const definition = HARNESS_DEFINITIONS[harness];\n  if (!definition) {\n    throw new Error(`Unsupported test-drive harness: ${String(harness)}.`);\n  }\n\n  const companyName = (options.companyName ?? \"Test Company\").trim();\n  const agentName = (options.agentName ?? \"CEO\").trim();\n  if (!companyName) throw new Error(\"--company-name cannot be empty.\");\n  if (!agentName) throw new Error(\"--agent-name cannot be empty.\");\n\n  const model = options.model;\n  if (model !== undefined && (!model || model.trim() !== model)) {\n    throw new Error(\"--model cannot be empty or have surrounding whitespace.\");\n  }\n  if (\n    harness === \"opencode\" &&\n    (!model || !/^openrouter\\/[^/\\s]+(?:\\/[^/\\s]+)*$/.test(model))\n  ) {\n    throw new Error(\n      \"OpenCode test drives require --model openrouter/<model>, with no empty path segments.\",\n    );\n  }\n\n  const sourceEnvName = options.apiKeyEnv?.trim() || definition.credentialTarget;\n  if (options.apiKeyEnv !== undefined && !/^[A-Za-z_][A-Za-z0-9_]*$/.test(sourceEnvName)) {\n    throw new Error(\"--api-key-env must name a valid environment variable.\");","sourceCodeStart":221,"sourceCodeEnd":257,"githubUrl":"https://github.com/paperclipai/paperclip/blob/01ad8584922b5d85292b1723cae71fa0d9b07a19/cli/src/commands/test-drive.ts#L221-L257","documentation":"When a semantic tool call arrives over PRP, the transport validates that the call's correlation (runId, normalizedSessionId, turnId) still matches the currently admitted turn identity on the active core. If the core instance changed, or any correlation field differs, the call is stale/belongs to another turn and is rejected with this error instead of being dispatched to the handler.","triggerScenarios":"A queued or in-flight tool call executes after a new core/turn was admitted: `call.correlation.runId`, `normalizedSessionId`, or `turnId` no longer equals `core.store.state.identity` values, or `core !== this.#core`.","commonSituations":"A turn was cancelled or rotated while a tool call was still pending; replayed events from an old run delivered to a new turn; concurrent turns where a client keeps submitting tool results from a superseded turn.","solutions":["Discard the stale tool call and re-issue it within the current turn's correlation context.","Ensure the client reads the current runId/sessionId/turnId from the admitted turn before sending tool responses.","Avoid caching tool-call handles across turn boundaries; refetch per turn."],"exampleFix":"// before\nawait respond(call, result); // stale correlation\n// after\nif (call.correlation.turnId === currentTurnId) await respond(call, result);\nelse await reissueToolCall(call, currentTurnId);","handlingStrategy":"type-guard","validationCode":"function callMatchesTurn(call, identity) {\n  return call.correlation.runId === identity.runId &&\n    call.correlation.normalizedSessionId === identity.normalizedSessionId &&\n    call.correlation.turnId === identity.turnId;\n}\nif (!callMatchesTurn(call, core.store.state.identity)) discardCall(call);","typeGuard":"function isCurrentTurnCall(call, core): call is ToolCall & { correlation: CurrentCorrelation } {\n  const id = core.store.state.identity;\n  return call.correlation.runId === id.runId &&\n    call.correlation.normalizedSessionId === id.normalizedSessionId &&\n    call.correlation.turnId === id.turnId;\n}","tryCatchPattern":"try {\n  await transport.respondToToolCall(call, result);\n} catch (e) {\n  if (e.message.includes(\"no longer belongs to an admitted turn\")) {\n    // drop stale call; refetch current turn identity and re-issue if still relevant\n  }\n}","preventionTips":["Never cache tool-call handles across turn boundaries.","Subscribe to turn-rotation events and cancel in-flight calls on rotation.","Include runId/sessionId/turnId checks client-side before submitting results."],"tags":["runner","tool-call","stale-state"],"backgroundTag":"invalid-state-transition","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"}