@tickernelz/paperclip-pro-adapter-claude-local 2026.925.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (206) hide show
  1. package/LICENSE +22 -0
  2. package/dist/cli/format-event.d.ts +2 -0
  3. package/dist/cli/format-event.d.ts.map +1 -0
  4. package/dist/cli/format-event.js +141 -0
  5. package/dist/cli/format-event.js.map +1 -0
  6. package/dist/cli/index.d.ts +2 -0
  7. package/dist/cli/index.d.ts.map +1 -0
  8. package/dist/cli/index.js +2 -0
  9. package/dist/cli/index.js.map +1 -0
  10. package/dist/cli/quota-probe.d.ts +3 -0
  11. package/dist/cli/quota-probe.d.ts.map +1 -0
  12. package/dist/cli/quota-probe.js +106 -0
  13. package/dist/cli/quota-probe.js.map +1 -0
  14. package/dist/index.d.ts +13 -0
  15. package/dist/index.d.ts.map +1 -0
  16. package/dist/index.js +90 -0
  17. package/dist/index.js.map +1 -0
  18. package/dist/index.test.d.ts +2 -0
  19. package/dist/index.test.d.ts.map +1 -0
  20. package/dist/index.test.js +35 -0
  21. package/dist/index.test.js.map +1 -0
  22. package/dist/server/acp.auth.test.d.ts +2 -0
  23. package/dist/server/acp.auth.test.d.ts.map +1 -0
  24. package/dist/server/acp.auth.test.js +581 -0
  25. package/dist/server/acp.auth.test.js.map +1 -0
  26. package/dist/server/acp.d.ts +68 -0
  27. package/dist/server/acp.d.ts.map +1 -0
  28. package/dist/server/acp.js +779 -0
  29. package/dist/server/acp.js.map +1 -0
  30. package/dist/server/acp.quota.test.d.ts +2 -0
  31. package/dist/server/acp.quota.test.d.ts.map +1 -0
  32. package/dist/server/acp.quota.test.js +114 -0
  33. package/dist/server/acp.quota.test.js.map +1 -0
  34. package/dist/server/acp.test.d.ts +2 -0
  35. package/dist/server/acp.test.d.ts.map +1 -0
  36. package/dist/server/acp.test.js +1153 -0
  37. package/dist/server/acp.test.js.map +1 -0
  38. package/dist/server/auth-check.d.ts +8 -0
  39. package/dist/server/auth-check.d.ts.map +1 -0
  40. package/dist/server/auth-check.js +8 -0
  41. package/dist/server/auth-check.js.map +1 -0
  42. package/dist/server/claude-config.d.ts +49 -0
  43. package/dist/server/claude-config.d.ts.map +1 -0
  44. package/dist/server/claude-config.js +279 -0
  45. package/dist/server/claude-config.js.map +1 -0
  46. package/dist/server/claude-config.test.d.ts +2 -0
  47. package/dist/server/claude-config.test.d.ts.map +1 -0
  48. package/dist/server/claude-config.test.js +197 -0
  49. package/dist/server/claude-config.test.js.map +1 -0
  50. package/dist/server/cli-capabilities.d.ts +26 -0
  51. package/dist/server/cli-capabilities.d.ts.map +1 -0
  52. package/dist/server/cli-capabilities.js +115 -0
  53. package/dist/server/cli-capabilities.js.map +1 -0
  54. package/dist/server/config-schema.d.ts +3 -0
  55. package/dist/server/config-schema.d.ts.map +1 -0
  56. package/dist/server/config-schema.js +67 -0
  57. package/dist/server/config-schema.js.map +1 -0
  58. package/dist/server/engine-availability.test.d.ts +2 -0
  59. package/dist/server/engine-availability.test.d.ts.map +1 -0
  60. package/dist/server/engine-availability.test.js +41 -0
  61. package/dist/server/engine-availability.test.js.map +1 -0
  62. package/dist/server/execute.acp-fallback.test.d.ts +2 -0
  63. package/dist/server/execute.acp-fallback.test.d.ts.map +1 -0
  64. package/dist/server/execute.acp-fallback.test.js +104 -0
  65. package/dist/server/execute.acp-fallback.test.js.map +1 -0
  66. package/dist/server/execute.d.ts +23 -0
  67. package/dist/server/execute.d.ts.map +1 -0
  68. package/dist/server/execute.js +1089 -0
  69. package/dist/server/execute.js.map +1 -0
  70. package/dist/server/execute.remote.test.d.ts +2 -0
  71. package/dist/server/execute.remote.test.d.ts.map +1 -0
  72. package/dist/server/execute.remote.test.js +484 -0
  73. package/dist/server/execute.remote.test.js.map +1 -0
  74. package/dist/server/index.d.ts +16 -0
  75. package/dist/server/index.d.ts.map +1 -0
  76. package/dist/server/index.js +84 -0
  77. package/dist/server/index.js.map +1 -0
  78. package/dist/server/models.d.ts +13 -0
  79. package/dist/server/models.d.ts.map +1 -0
  80. package/dist/server/models.js +153 -0
  81. package/dist/server/models.js.map +1 -0
  82. package/dist/server/parse.d.ts +58 -0
  83. package/dist/server/parse.d.ts.map +1 -0
  84. package/dist/server/parse.js +459 -0
  85. package/dist/server/parse.js.map +1 -0
  86. package/dist/server/parse.test.d.ts +2 -0
  87. package/dist/server/parse.test.d.ts.map +1 -0
  88. package/dist/server/parse.test.js +419 -0
  89. package/dist/server/parse.test.js.map +1 -0
  90. package/dist/server/permissions.d.ts +14 -0
  91. package/dist/server/permissions.d.ts.map +1 -0
  92. package/dist/server/permissions.js +13 -0
  93. package/dist/server/permissions.js.map +1 -0
  94. package/dist/server/permissions.test.d.ts +2 -0
  95. package/dist/server/permissions.test.d.ts.map +1 -0
  96. package/dist/server/permissions.test.js +22 -0
  97. package/dist/server/permissions.test.js.map +1 -0
  98. package/dist/server/probe-diagnostics.d.ts +102 -0
  99. package/dist/server/probe-diagnostics.d.ts.map +1 -0
  100. package/dist/server/probe-diagnostics.js +140 -0
  101. package/dist/server/probe-diagnostics.js.map +1 -0
  102. package/dist/server/probe-diagnostics.test.d.ts +2 -0
  103. package/dist/server/probe-diagnostics.test.d.ts.map +1 -0
  104. package/dist/server/probe-diagnostics.test.js +114 -0
  105. package/dist/server/probe-diagnostics.test.js.map +1 -0
  106. package/dist/server/probe-env.d.ts +42 -0
  107. package/dist/server/probe-env.d.ts.map +1 -0
  108. package/dist/server/probe-env.js +149 -0
  109. package/dist/server/probe-env.js.map +1 -0
  110. package/dist/server/probe-env.test.d.ts +2 -0
  111. package/dist/server/probe-env.test.d.ts.map +1 -0
  112. package/dist/server/probe-env.test.js +135 -0
  113. package/dist/server/probe-env.test.js.map +1 -0
  114. package/dist/server/probe-redaction.test.d.ts +2 -0
  115. package/dist/server/probe-redaction.test.d.ts.map +1 -0
  116. package/dist/server/probe-redaction.test.js +86 -0
  117. package/dist/server/probe-redaction.test.js.map +1 -0
  118. package/dist/server/prompt-cache.d.ts +17 -0
  119. package/dist/server/prompt-cache.d.ts.map +1 -0
  120. package/dist/server/prompt-cache.js +126 -0
  121. package/dist/server/prompt-cache.js.map +1 -0
  122. package/dist/server/quota-keychain.test.d.ts +2 -0
  123. package/dist/server/quota-keychain.test.d.ts.map +1 -0
  124. package/dist/server/quota-keychain.test.js +83 -0
  125. package/dist/server/quota-keychain.test.js.map +1 -0
  126. package/dist/server/quota.d.ts +30 -0
  127. package/dist/server/quota.d.ts.map +1 -0
  128. package/dist/server/quota.js +543 -0
  129. package/dist/server/quota.js.map +1 -0
  130. package/dist/server/setup-token-characterization.test.d.ts +2 -0
  131. package/dist/server/setup-token-characterization.test.d.ts.map +1 -0
  132. package/dist/server/setup-token-characterization.test.js +123 -0
  133. package/dist/server/setup-token-characterization.test.js.map +1 -0
  134. package/dist/server/setup-token-parse.d.ts +47 -0
  135. package/dist/server/setup-token-parse.d.ts.map +1 -0
  136. package/dist/server/setup-token-parse.js +506 -0
  137. package/dist/server/setup-token-parse.js.map +1 -0
  138. package/dist/server/setup-token-parse.test.d.ts +2 -0
  139. package/dist/server/setup-token-parse.test.d.ts.map +1 -0
  140. package/dist/server/setup-token-parse.test.js +587 -0
  141. package/dist/server/setup-token-parse.test.js.map +1 -0
  142. package/dist/server/setup-token-runner.d.ts +115 -0
  143. package/dist/server/setup-token-runner.d.ts.map +1 -0
  144. package/dist/server/setup-token-runner.js +317 -0
  145. package/dist/server/setup-token-runner.js.map +1 -0
  146. package/dist/server/setup-token-runner.test.d.ts +2 -0
  147. package/dist/server/setup-token-runner.test.d.ts.map +1 -0
  148. package/dist/server/setup-token-runner.test.js +336 -0
  149. package/dist/server/setup-token-runner.test.js.map +1 -0
  150. package/dist/server/skills.d.ts +8 -0
  151. package/dist/server/skills.d.ts.map +1 -0
  152. package/dist/server/skills.js +42 -0
  153. package/dist/server/skills.js.map +1 -0
  154. package/dist/server/test.d.ts +3 -0
  155. package/dist/server/test.d.ts.map +1 -0
  156. package/dist/server/test.js +434 -0
  157. package/dist/server/test.js.map +1 -0
  158. package/dist/server/test.probe.test.d.ts +2 -0
  159. package/dist/server/test.probe.test.d.ts.map +1 -0
  160. package/dist/server/test.probe.test.js +608 -0
  161. package/dist/server/test.probe.test.js.map +1 -0
  162. package/dist/server/test.remote.test.d.ts +2 -0
  163. package/dist/server/test.remote.test.d.ts.map +1 -0
  164. package/dist/server/test.remote.test.js +149 -0
  165. package/dist/server/test.remote.test.js.map +1 -0
  166. package/dist/ui/build-config.d.ts +3 -0
  167. package/dist/ui/build-config.d.ts.map +1 -0
  168. package/dist/ui/build-config.js +76 -0
  169. package/dist/ui/build-config.js.map +1 -0
  170. package/dist/ui/build-config.test.d.ts +2 -0
  171. package/dist/ui/build-config.test.d.ts.map +1 -0
  172. package/dist/ui/build-config.test.js +68 -0
  173. package/dist/ui/build-config.test.js.map +1 -0
  174. package/dist/ui/index.d.ts +3 -0
  175. package/dist/ui/index.d.ts.map +1 -0
  176. package/dist/ui/index.js +3 -0
  177. package/dist/ui/index.js.map +1 -0
  178. package/dist/ui/parse-stdout.d.ts +3 -0
  179. package/dist/ui/parse-stdout.d.ts.map +1 -0
  180. package/dist/ui/parse-stdout.js +153 -0
  181. package/dist/ui/parse-stdout.js.map +1 -0
  182. package/package.json +63 -0
  183. package/skills/agentmail/SKILL.md +70 -0
  184. package/skills/paperclip/SKILL.md +712 -0
  185. package/skills/paperclip/references/api-reference.md +1675 -0
  186. package/skills/paperclip/references/artifacts.md +158 -0
  187. package/skills/paperclip/references/cases.md +295 -0
  188. package/skills/paperclip/references/company-skills.md +266 -0
  189. package/skills/paperclip/references/issue-workspaces.md +80 -0
  190. package/skills/paperclip/references/routines.md +231 -0
  191. package/skills/paperclip/references/workflows.md +141 -0
  192. package/skills/paperclip/scripts/paperclip-upload-artifact.sh +592 -0
  193. package/skills/paperclip-board/SKILL.md +619 -0
  194. package/skills/paperclip-converting-plans-to-tasks/SKILL.md +60 -0
  195. package/skills/paperclip-create-agent/SKILL.md +179 -0
  196. package/skills/paperclip-create-agent/references/agent-instruction-templates.md +123 -0
  197. package/skills/paperclip-create-agent/references/agents/coder.md +64 -0
  198. package/skills/paperclip-create-agent/references/agents/qa.md +88 -0
  199. package/skills/paperclip-create-agent/references/agents/securityengineer.md +135 -0
  200. package/skills/paperclip-create-agent/references/agents/uxdesigner.md +115 -0
  201. package/skills/paperclip-create-agent/references/api-reference.md +110 -0
  202. package/skills/paperclip-create-agent/references/baseline-role-guide.md +168 -0
  203. package/skills/paperclip-create-agent/references/draft-review-checklist.md +95 -0
  204. package/skills/para-memory-files/SKILL.md +100 -0
  205. package/skills/para-memory-files/references/schemas.md +35 -0
  206. package/skills/slack/SKILL.md +65 -0
@@ -0,0 +1,1089 @@
1
+ import fs from "node:fs/promises";
2
+ import path from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
+ import { adapterExecutionTargetIsRemote, adapterExecutionTargetRemoteCwd, overrideAdapterExecutionTargetRemoteCwd, adapterExecutionTargetSessionIdentity, adapterExecutionTargetSessionMatches, adapterExecutionTargetUsesManagedHome, adapterExecutionTargetUsesPaperclipBridge, describeAdapterExecutionTarget, ensureAdapterExecutionTargetCommandResolvable, ensureAdapterExecutionTargetRuntimeCommandInstalled, prepareAdapterExecutionTargetRuntime, adapterExecutionTargetDuplexObservabilityRecorder, adapterExecutionTargetEnablesSandboxDuplexBridge, readAdapterExecutionTarget, resolveAdapterExecutionTargetTimeoutSec, resolveAdapterExecutionTargetCommandForLogs, runAdapterExecutionTargetProcess, startAdapterExecutionTargetPaperclipBridge, } from "@tickernelz/paperclip-pro-adapter-utils/execution-target";
5
+ import { asString, asNumber, asBoolean, asStringArray, parseObject, parseJson, applyPaperclipWorkspaceEnv, buildPaperclipEnv, isPaperclipSkillSourceMissing, readPaperclipRuntimeSkillEntries, readPaperclipIssueWorkModeFromContext, joinPromptSections, buildInvocationEnvForLogs, ensureAbsoluteDirectory, ensurePathInEnv, isForbiddenConfigEnvKey, isPaperclipRuntimeEnvKey, refreshPaperclipWorkspaceEnvForExecution, renderTemplate, renderPaperclipWakePrompt, isPaperclipRecoveryWakePayload, selectPaperclipTaskMarkdown, rewriteWorkspaceCwdEnvVarsForExecution, shapePaperclipWorkspaceEnvForExecution, DEFAULT_PAPERCLIP_AGENT_PROMPT_TEMPLATE, DEFAULT_PAPERCLIP_CONVERSATION_PROMPT_TEMPLATE, } from "@tickernelz/paperclip-pro-adapter-utils/server-utils";
6
+ import { buildSkillLibraryManifestMarkdown } from "@tickernelz/paperclip-pro-adapter-utils/skill-library-manifest";
7
+ import { parseLocalProcessFilesystemScope, parseLocalProcessSandboxExtraPaths, parseLocalProcessNetworkAllowlist, parseLocalProcessNetworkScope, } from "@tickernelz/paperclip-pro-adapter-utils/local-process-sandbox";
8
+ import { claudeModelUsageTotals, parseClaudeStreamJson, describeClaudeFailure, detectClaudeLoginRequired, extractClaudeRetryNotBefore, isClaudeMaxTurnsResult, isClaudeProviderQuotaError, isClaudeRefusalResult, isClaudeTransientUpstreamError, isClaudeUnknownSessionError, isClaudePoisonedPreviousMessageIdError, isClaudeImageProcessingError, isClaudeModelNotFoundError, } from "./parse.js";
9
+ import { materializeRemoteClaudeConfig, prepareClaudeConfigSeed, resolveManagedClaudeRuntimeStateDir, resolveSharedClaudeConfigDir, writePaperclipClaudeMcpConfig, } from "./claude-config.js";
10
+ import { claudeCliVersionAtLeast, claudeCommandLooksLike, claudeCommandSupportsEffortFlag, minimumClaudeCliVersionForModel, readClaudeCommandVersion, } from "./cli-capabilities.js";
11
+ import { resolveClaudeDesiredSkillNames } from "./skills.js";
12
+ import { isBedrockModelId } from "./models.js";
13
+ import { prepareClaudePromptBundle } from "./prompt-cache.js";
14
+ import { buildClaudeExecutionPermissionArgs, claudeSandboxPermissionEnv } from "./permissions.js";
15
+ import { resolveClaudeModel, SANDBOX_INSTALL_COMMAND } from "../index.js";
16
+ import { createClaudeAcpExecutor, resolveClaudeExecutionEngineForRun, } from "./acp.js";
17
+ const __moduleDir = path.dirname(fileURLToPath(import.meta.url));
18
+ const executeClaudeAcp = createClaudeAcpExecutor();
19
+ export function claudeSessionCwdMatchesExecutionTarget(input) {
20
+ if (input.executionTargetIsRemote || input.runtimeSessionCwd.length === 0)
21
+ return true;
22
+ return path.resolve(input.runtimeSessionCwd) === path.resolve(input.effectiveExecutionCwd);
23
+ }
24
+ function buildLoginResult(input) {
25
+ return {
26
+ exitCode: input.proc.exitCode,
27
+ signal: input.proc.signal,
28
+ timedOut: input.proc.timedOut,
29
+ stdout: input.proc.stdout,
30
+ stderr: input.proc.stderr,
31
+ loginUrl: input.loginUrl,
32
+ };
33
+ }
34
+ function hasNonEmptyEnvValue(env, key) {
35
+ const raw = env[key];
36
+ return typeof raw === "string" && raw.trim().length > 0;
37
+ }
38
+ function isBedrockAuth(env) {
39
+ return (env.CLAUDE_CODE_USE_BEDROCK === "1" ||
40
+ env.CLAUDE_CODE_USE_BEDROCK === "true" ||
41
+ hasNonEmptyEnvValue(env, "ANTHROPIC_BEDROCK_BASE_URL"));
42
+ }
43
+ function resolveClaudeBillingType(env) {
44
+ if (isBedrockAuth(env))
45
+ return "metered_api";
46
+ return hasNonEmptyEnvValue(env, "ANTHROPIC_API_KEY") ? "api" : "subscription";
47
+ }
48
+ async function buildClaudeRuntimeConfig(input) {
49
+ const { runId, agent, config, context, runtimeCommandSpec, executionTarget, authToken } = input;
50
+ const onLog = input.onLog ?? (async () => { });
51
+ const command = asString(config.command, "claude");
52
+ const workspaceContext = parseObject(context.paperclipWorkspace);
53
+ const workspaceCwd = asString(workspaceContext.cwd, "");
54
+ const workspaceSource = asString(workspaceContext.source, "");
55
+ const workspaceStrategy = asString(workspaceContext.strategy, "");
56
+ const workspaceId = asString(workspaceContext.workspaceId, "") || null;
57
+ const workspaceRepoUrl = asString(workspaceContext.repoUrl, "") || null;
58
+ const workspaceRepoRef = asString(workspaceContext.repoRef, "") || null;
59
+ const workspaceBranch = asString(workspaceContext.branchName, "") || null;
60
+ const workspaceWorktreePath = asString(workspaceContext.worktreePath, "") || null;
61
+ const agentHome = asString(workspaceContext.agentHome, "") || null;
62
+ const workspaceHints = Array.isArray(context.paperclipWorkspaces)
63
+ ? context.paperclipWorkspaces.filter((value) => typeof value === "object" && value !== null)
64
+ : [];
65
+ const runtimeServiceIntents = Array.isArray(context.paperclipRuntimeServiceIntents)
66
+ ? context.paperclipRuntimeServiceIntents.filter((value) => typeof value === "object" && value !== null)
67
+ : [];
68
+ const runtimeServices = Array.isArray(context.paperclipRuntimeServices)
69
+ ? context.paperclipRuntimeServices.filter((value) => typeof value === "object" && value !== null)
70
+ : [];
71
+ const runtimePrimaryUrl = asString(context.paperclipRuntimePrimaryUrl, "");
72
+ const configuredCwd = asString(config.cwd, "");
73
+ const useConfiguredInsteadOfAgentHome = workspaceSource === "agent_home" && configuredCwd.length > 0;
74
+ const effectiveWorkspaceCwd = useConfiguredInsteadOfAgentHome ? "" : workspaceCwd;
75
+ const cwd = effectiveWorkspaceCwd || configuredCwd || process.cwd();
76
+ const executionTargetIsRemote = adapterExecutionTargetIsRemote(executionTarget);
77
+ let effectiveExecutionCwd = adapterExecutionTargetRemoteCwd(executionTarget, cwd);
78
+ const shapedWorkspaceEnv = shapePaperclipWorkspaceEnvForExecution({
79
+ workspaceCwd: effectiveWorkspaceCwd,
80
+ workspaceWorktreePath,
81
+ workspaceHints,
82
+ executionTargetIsRemote,
83
+ executionCwd: effectiveExecutionCwd,
84
+ });
85
+ await ensureAbsoluteDirectory(cwd, { createIfMissing: true });
86
+ const envConfig = parseObject(config.env);
87
+ const env = { ...buildPaperclipEnv(agent) };
88
+ env.PAPERCLIP_RUN_ID = runId;
89
+ const wakeTaskId = (typeof context.taskId === "string" && context.taskId.trim().length > 0 && context.taskId.trim()) ||
90
+ (typeof context.issueId === "string" && context.issueId.trim().length > 0 && context.issueId.trim()) ||
91
+ null;
92
+ const wakeReason = typeof context.wakeReason === "string" && context.wakeReason.trim().length > 0
93
+ ? context.wakeReason.trim()
94
+ : null;
95
+ const wakeCommentId = (typeof context.wakeCommentId === "string" && context.wakeCommentId.trim().length > 0 && context.wakeCommentId.trim()) ||
96
+ (typeof context.commentId === "string" && context.commentId.trim().length > 0 && context.commentId.trim()) ||
97
+ null;
98
+ const approvalId = typeof context.approvalId === "string" && context.approvalId.trim().length > 0
99
+ ? context.approvalId.trim()
100
+ : null;
101
+ const approvalStatus = typeof context.approvalStatus === "string" && context.approvalStatus.trim().length > 0
102
+ ? context.approvalStatus.trim()
103
+ : null;
104
+ const linkedIssueIds = Array.isArray(context.issueIds)
105
+ ? context.issueIds.filter((value) => typeof value === "string" && value.trim().length > 0)
106
+ : [];
107
+ const issueWorkMode = readPaperclipIssueWorkModeFromContext(context);
108
+ if (wakeTaskId) {
109
+ env.PAPERCLIP_TASK_ID = wakeTaskId;
110
+ }
111
+ if (issueWorkMode) {
112
+ env.PAPERCLIP_ISSUE_WORK_MODE = issueWorkMode;
113
+ }
114
+ if (wakeReason) {
115
+ env.PAPERCLIP_WAKE_REASON = wakeReason;
116
+ }
117
+ if (wakeCommentId) {
118
+ env.PAPERCLIP_WAKE_COMMENT_ID = wakeCommentId;
119
+ }
120
+ if (approvalId) {
121
+ env.PAPERCLIP_APPROVAL_ID = approvalId;
122
+ }
123
+ if (approvalStatus) {
124
+ env.PAPERCLIP_APPROVAL_STATUS = approvalStatus;
125
+ }
126
+ if (linkedIssueIds.length > 0) {
127
+ env.PAPERCLIP_LINKED_ISSUE_IDS = linkedIssueIds.join(",");
128
+ }
129
+ applyPaperclipWorkspaceEnv(env, {
130
+ workspaceCwd: shapedWorkspaceEnv.workspaceCwd,
131
+ workspaceSource,
132
+ workspaceStrategy,
133
+ workspaceId,
134
+ workspaceRepoUrl,
135
+ workspaceRepoRef,
136
+ workspaceBranch,
137
+ workspaceWorktreePath: shapedWorkspaceEnv.workspaceWorktreePath,
138
+ agentHome,
139
+ });
140
+ if (shapedWorkspaceEnv.workspaceHints.length > 0) {
141
+ env.PAPERCLIP_WORKSPACES_JSON = JSON.stringify(shapedWorkspaceEnv.workspaceHints);
142
+ }
143
+ if (runtimeServiceIntents.length > 0) {
144
+ env.PAPERCLIP_RUNTIME_SERVICE_INTENTS_JSON = JSON.stringify(runtimeServiceIntents);
145
+ }
146
+ if (runtimeServices.length > 0) {
147
+ env.PAPERCLIP_RUNTIME_SERVICES_JSON = JSON.stringify(runtimeServices);
148
+ }
149
+ if (runtimePrimaryUrl) {
150
+ env.PAPERCLIP_RUNTIME_PRIMARY_URL = runtimePrimaryUrl;
151
+ }
152
+ const shapedEnvConfig = rewriteWorkspaceCwdEnvVarsForExecution({
153
+ env: envConfig,
154
+ workspaceCwd: effectiveWorkspaceCwd,
155
+ executionCwd: shapedWorkspaceEnv.workspaceCwd,
156
+ executionTargetIsRemote,
157
+ });
158
+ for (const [key, value] of Object.entries(shapedEnvConfig)) {
159
+ if (typeof value !== "string")
160
+ continue;
161
+ // Runtime PAPERCLIP_* always wins over config, and PAPERCLIP_API_KEY is
162
+ // never accepted from config — the harness-minted run token is the only
163
+ // source. Other PAPERCLIP_* keys Paperclip did not assign flow through.
164
+ if (isForbiddenConfigEnvKey(key))
165
+ continue;
166
+ if (isPaperclipRuntimeEnvKey(key) && key in env)
167
+ continue;
168
+ env[key] = value;
169
+ }
170
+ if (authToken) {
171
+ env.PAPERCLIP_API_KEY = authToken;
172
+ }
173
+ const runtimeEnv = Object.fromEntries(Object.entries(ensurePathInEnv({ ...process.env, ...env })).filter((entry) => typeof entry[1] === "string"));
174
+ const timeoutSec = resolveAdapterExecutionTargetTimeoutSec(executionTarget, asNumber(config.timeoutSec, 0));
175
+ const graceSec = asNumber(config.graceSec, 20);
176
+ await ensureAdapterExecutionTargetRuntimeCommandInstalled({
177
+ runId,
178
+ target: executionTarget,
179
+ installCommand: runtimeCommandSpec?.installCommand,
180
+ detectCommand: runtimeCommandSpec?.detectCommand,
181
+ cwd,
182
+ env: runtimeEnv,
183
+ timeoutSec,
184
+ graceSec,
185
+ onLog,
186
+ });
187
+ await ensureAdapterExecutionTargetCommandResolvable(command, executionTarget, cwd, runtimeEnv, {
188
+ installCommand: SANDBOX_INSTALL_COMMAND,
189
+ timeoutSec,
190
+ });
191
+ const resolvedCommand = await resolveAdapterExecutionTargetCommandForLogs(command, executionTarget, cwd, runtimeEnv);
192
+ const loggedEnv = buildInvocationEnvForLogs(env, {
193
+ runtimeEnv,
194
+ includeRuntimeKeys: ["HOME", "CLAUDE_CONFIG_DIR"],
195
+ resolvedCommand,
196
+ });
197
+ const extraArgs = (() => {
198
+ const fromExtraArgs = asStringArray(config.extraArgs);
199
+ if (fromExtraArgs.length > 0)
200
+ return fromExtraArgs;
201
+ return asStringArray(config.args);
202
+ })();
203
+ return {
204
+ command,
205
+ resolvedCommand,
206
+ cwd,
207
+ workspaceId,
208
+ workspaceRepoUrl,
209
+ workspaceRepoRef,
210
+ env,
211
+ loggedEnv,
212
+ timeoutSec,
213
+ graceSec,
214
+ extraArgs,
215
+ };
216
+ }
217
+ export async function runClaudeLogin(input) {
218
+ const onLog = input.onLog ?? (async () => { });
219
+ const runtime = await buildClaudeRuntimeConfig({
220
+ runId: input.runId,
221
+ agent: input.agent,
222
+ config: input.config,
223
+ context: input.context ?? {},
224
+ authToken: input.authToken,
225
+ });
226
+ const proc = await runAdapterExecutionTargetProcess(input.runId, null, runtime.command, ["login"], {
227
+ cwd: runtime.cwd,
228
+ env: runtime.env,
229
+ timeoutSec: runtime.timeoutSec,
230
+ graceSec: runtime.graceSec,
231
+ onLog,
232
+ });
233
+ const loginMeta = detectClaudeLoginRequired({
234
+ parsed: null,
235
+ stdout: proc.stdout,
236
+ stderr: proc.stderr,
237
+ });
238
+ return buildLoginResult({
239
+ proc,
240
+ loginUrl: loginMeta.loginUrl,
241
+ });
242
+ }
243
+ export async function execute(ctx) {
244
+ const engineSelection = await resolveClaudeExecutionEngineForRun(ctx);
245
+ if (engineSelection.unavailableReason) {
246
+ return {
247
+ exitCode: 1,
248
+ signal: null,
249
+ timedOut: false,
250
+ errorCode: "adapter_engine_unavailable",
251
+ errorMessage: engineSelection.unavailableReason,
252
+ resultJson: {
253
+ executionRecovery: { kind: "bootstrap", providerWorkStarted: false },
254
+ },
255
+ };
256
+ }
257
+ if (engineSelection.engine === "acp") {
258
+ return executeClaudeAcp(ctx);
259
+ }
260
+ const { runId, agent, runtime, config, context, onLog, onMeta, onSpawn, authToken } = ctx;
261
+ const executionTarget = readAdapterExecutionTarget({
262
+ executionTarget: ctx.executionTarget,
263
+ legacyRemoteExecution: ctx.executionTransport?.remoteExecution,
264
+ });
265
+ const executionTargetIsRemote = adapterExecutionTargetIsRemote(executionTarget);
266
+ const executionTargetIsSandbox = executionTarget?.kind === "remote" && executionTarget.transport === "sandbox";
267
+ const promptTemplate = asString(config.promptTemplate, context.conversationMode === true
268
+ ? DEFAULT_PAPERCLIP_CONVERSATION_PROMPT_TEMPLATE
269
+ : DEFAULT_PAPERCLIP_AGENT_PROMPT_TEMPLATE);
270
+ const effort = asString(config.effort, "");
271
+ const chrome = asBoolean(config.chrome, false);
272
+ const maxTurns = asNumber(config.maxTurnsPerRun, 0);
273
+ const dangerouslySkipPermissions = asBoolean(config.dangerouslySkipPermissions, true);
274
+ const configEnv = parseObject(config.env);
275
+ const workspaceContext = parseObject(context.paperclipWorkspace);
276
+ const workspaceCwd = asString(workspaceContext.cwd, "");
277
+ const workspaceSource = asString(workspaceContext.source, "");
278
+ const workspaceStrategy = asString(workspaceContext.strategy, "");
279
+ const workspaceBranch = asString(workspaceContext.branchName, "") || null;
280
+ const workspaceWorktreePath = asString(workspaceContext.worktreePath, "") || null;
281
+ const agentHome = asString(workspaceContext.agentHome, "") || null;
282
+ const workspaceHints = Array.isArray(context.paperclipWorkspaces)
283
+ ? context.paperclipWorkspaces.filter((value) => typeof value === "object" && value !== null)
284
+ : [];
285
+ const configuredCwd = asString(config.cwd, "");
286
+ const useConfiguredInsteadOfAgentHome = workspaceSource === "agent_home" && configuredCwd.length > 0;
287
+ const effectiveWorkspaceCwd = useConfiguredInsteadOfAgentHome ? "" : workspaceCwd;
288
+ const hasExplicitClaudeConfigDir = typeof configEnv.CLAUDE_CONFIG_DIR === "string" && configEnv.CLAUDE_CONFIG_DIR.trim().length > 0;
289
+ const instructionsFilePath = asString(config.instructionsFilePath, "").trim();
290
+ const instructionsFileDir = instructionsFilePath ? `${path.dirname(instructionsFilePath)}/` : "";
291
+ const runtimeConfig = await buildClaudeRuntimeConfig({
292
+ runId,
293
+ agent,
294
+ config,
295
+ context,
296
+ runtimeCommandSpec: ctx.runtimeCommandSpec,
297
+ executionTarget,
298
+ authToken,
299
+ onLog,
300
+ });
301
+ const { command, resolvedCommand, cwd, workspaceId, workspaceRepoUrl, workspaceRepoRef, env, loggedEnv: initialLoggedEnv, timeoutSec, graceSec, extraArgs, } = runtimeConfig;
302
+ Object.assign(env, claudeSandboxPermissionEnv({ dangerouslySkipPermissions, targetIsSandbox: executionTargetIsSandbox }));
303
+ let loggedEnv = initialLoggedEnv;
304
+ let effectiveExecutionCwd = adapterExecutionTargetRemoteCwd(executionTarget, cwd);
305
+ const terminalResultCleanupGraceMs = Math.max(0, asNumber(config.terminalResultCleanupGraceMs, 5_000));
306
+ const effectiveEnv = Object.fromEntries(Object.entries({ ...process.env, ...env }).filter((entry) => typeof entry[1] === "string"));
307
+ const modelEnv = executionTargetIsRemote ? env : effectiveEnv;
308
+ const model = resolveClaudeModel(config.model, modelEnv);
309
+ const billingType = resolveClaudeBillingType(effectiveEnv);
310
+ const claudeSkillEntries = await readPaperclipRuntimeSkillEntries(config, __moduleDir);
311
+ const desiredSkillNames = new Set(resolveClaudeDesiredSkillNames(config, claudeSkillEntries));
312
+ // When instructionsFilePath is configured, build a stable content-addressed
313
+ // file that includes both the file content and the path directive, so we only
314
+ // need --append-system-prompt-file (Claude CLI forbids using both flags together).
315
+ let combinedInstructionsContents = null;
316
+ if (instructionsFilePath) {
317
+ try {
318
+ const instructionsContent = await fs.readFile(instructionsFilePath, "utf-8");
319
+ const pathDirective = `\nThe above agent instructions were loaded from ${instructionsFilePath}. ` +
320
+ `Resolve any relative file references from ${instructionsFileDir}. ` +
321
+ `This base directory is authoritative for sibling instruction files such as ` +
322
+ `./HEARTBEAT.md, ./SOUL.md, and ./TOOLS.md; do not resolve those from the parent agent directory.`;
323
+ combinedInstructionsContents = instructionsContent + pathDirective;
324
+ }
325
+ catch (err) {
326
+ const reason = err instanceof Error ? err.message : String(err);
327
+ await onLog("stderr", `[paperclip] Warning: could not read agent instructions file "${instructionsFilePath}": ${reason}\n`);
328
+ }
329
+ }
330
+ // Tell the model what the company library actually holds. Without this, an
331
+ // installed-but-not-enabled skill is indistinguishable from a nonexistent
332
+ // one from inside the sandbox, and agents tell users freshly installed
333
+ // skills "are not installed". Deterministic text appended to the
334
+ // instructions, so it participates in the prompt-bundle cache key and only
335
+ // busts the cache when the library really changes.
336
+ const skillLibraryManifest = buildSkillLibraryManifestMarkdown({
337
+ entries: claudeSkillEntries,
338
+ desiredSkillKeys: desiredSkillNames,
339
+ });
340
+ if (skillLibraryManifest) {
341
+ combinedInstructionsContents = combinedInstructionsContents
342
+ ? `${combinedInstructionsContents}\n\n${skillLibraryManifest}`
343
+ : skillLibraryManifest;
344
+ }
345
+ // Missing-source entries must never reach the bundle: their path does not
346
+ // exist, so the bundle hasher would throw and fail the whole run over one
347
+ // broken skill. Log each one instead so the cause lands in the run output.
348
+ const desiredSkillEntries = claudeSkillEntries.filter((entry) => desiredSkillNames.has(entry.key));
349
+ const mountableSkillEntries = desiredSkillEntries.filter((entry) => !isPaperclipSkillSourceMissing(entry));
350
+ for (const entry of desiredSkillEntries) {
351
+ if (!isPaperclipSkillSourceMissing(entry))
352
+ continue;
353
+ await onLog("stderr", `[paperclip] Warning: skill "${entry.key}" is enabled for this agent but its files are unavailable and it was not mounted${entry.missingDetail ? `: ${entry.missingDetail}` : "."}\n`);
354
+ }
355
+ const promptBundle = await prepareClaudePromptBundle({
356
+ companyId: agent.companyId,
357
+ skills: mountableSkillEntries,
358
+ instructionsContents: combinedInstructionsContents,
359
+ onLog,
360
+ });
361
+ const runtimeMcpServers = ctx.runtimeMcp?.getServers() ?? [];
362
+ const runtimeMcpIdentity = JSON.stringify(runtimeMcpServers.map(({ name, url, connectionId }) => ({ name, url, connectionId })));
363
+ const claudeRuntimeStateDir = resolveManagedClaudeRuntimeStateDir(process.env, agent.companyId, agent.id);
364
+ const localMcpConfigPath = await writePaperclipClaudeMcpConfig({
365
+ stateDir: claudeRuntimeStateDir,
366
+ runId,
367
+ servers: runtimeMcpServers,
368
+ });
369
+ const localMcpConfigDir = path.dirname(localMcpConfigPath);
370
+ const sharedClaudeConfigDir = config.managedAiConnection ? asString(configEnv.CLAUDE_CONFIG_DIR, "") : resolveSharedClaudeConfigDir(process.env);
371
+ const networkScope = parseLocalProcessNetworkScope(config.networkScope);
372
+ const filesystemScope = parseLocalProcessFilesystemScope(config.filesystemScope);
373
+ const localProcessSandbox = (filesystemScope || networkScope) && !executionTargetIsRemote
374
+ ? {
375
+ workspaceDir: effectiveExecutionCwd,
376
+ filesystemScope,
377
+ managedPaths: [
378
+ { path: sharedClaudeConfigDir, access: "rw" },
379
+ { path: path.join(path.dirname(sharedClaudeConfigDir), ".claude.json"), access: "rw" },
380
+ { path: promptBundle.addDir, access: "ro" },
381
+ { path: localMcpConfigDir, access: "ro" },
382
+ ],
383
+ extraPaths: parseLocalProcessSandboxExtraPaths(config.filesystemExtraPaths),
384
+ homeDir: filesystemScope ? path.dirname(sharedClaudeConfigDir) : null,
385
+ networkScope,
386
+ networkAllowlist: parseLocalProcessNetworkAllowlist(config.networkAllowlist),
387
+ networkTrustedUrls: [
388
+ env.PAPERCLIP_API_URL,
389
+ ...runtimeMcpServers.map((server) => server.url),
390
+ ].filter((value) => typeof value === "string" && value.length > 0),
391
+ command: asString(config.filesystemSandboxCommand, "bwrap"),
392
+ }
393
+ : null;
394
+ if (localProcessSandbox) {
395
+ if (filesystemScope)
396
+ env.CLAUDE_CONFIG_DIR = sharedClaudeConfigDir;
397
+ const scopes = [filesystemScope ? "workspace filesystem" : null, networkScope ? `${networkScope} network` : null]
398
+ .filter(Boolean)
399
+ .join(" and ");
400
+ await onLog("stdout", `[paperclip] Confining Claude with ${scopes} scope.\n`);
401
+ }
402
+ const useManagedRemoteClaudeConfig = executionTargetIsRemote &&
403
+ adapterExecutionTargetUsesManagedHome(executionTarget) &&
404
+ (!hasExplicitClaudeConfigDir || Boolean(config.managedAiConnection));
405
+ const claudeConfigSeedDir = useManagedRemoteClaudeConfig
406
+ ? config.managedAiConnection ? sharedClaudeConfigDir : await prepareClaudeConfigSeed(process.env, onLog, agent.companyId)
407
+ : null;
408
+ const preparedExecutionTargetRuntime = executionTargetIsRemote
409
+ ? await (async () => {
410
+ await onLog("stdout", `[paperclip] Syncing workspace and Claude runtime assets to ${describeAdapterExecutionTarget(executionTarget)}.\n`);
411
+ return await prepareAdapterExecutionTargetRuntime({
412
+ runId,
413
+ target: executionTarget,
414
+ adapterKey: "claude",
415
+ timeoutSec,
416
+ workspaceLocalDir: cwd,
417
+ installCommand: SANDBOX_INSTALL_COMMAND,
418
+ detectCommand: command,
419
+ onProgress: (line) => onLog("stdout", line),
420
+ onRuntimeProgress: ctx.onRuntimeProgress,
421
+ assets: [
422
+ {
423
+ key: "skills",
424
+ localDir: promptBundle.addDir,
425
+ followSymlinks: true,
426
+ },
427
+ {
428
+ key: "mcp-config",
429
+ localDir: localMcpConfigDir,
430
+ followSymlinks: true,
431
+ },
432
+ ...(claudeConfigSeedDir
433
+ ? [{
434
+ key: "config-seed",
435
+ localDir: claudeConfigSeedDir,
436
+ followSymlinks: true,
437
+ }]
438
+ : []),
439
+ ],
440
+ });
441
+ })()
442
+ : null;
443
+ if (preparedExecutionTargetRuntime?.workspaceRemoteDir) {
444
+ effectiveExecutionCwd = preparedExecutionTargetRuntime.workspaceRemoteDir;
445
+ }
446
+ const runtimeExecutionTarget = overrideAdapterExecutionTargetRemoteCwd(executionTarget, effectiveExecutionCwd);
447
+ refreshPaperclipWorkspaceEnvForExecution({
448
+ env,
449
+ envConfig: configEnv,
450
+ workspaceCwd: effectiveWorkspaceCwd,
451
+ workspaceSource,
452
+ workspaceStrategy,
453
+ workspaceId,
454
+ workspaceRepoUrl,
455
+ workspaceRepoRef,
456
+ workspaceBranch,
457
+ workspaceWorktreePath,
458
+ workspaceHints,
459
+ agentHome,
460
+ executionTargetIsRemote,
461
+ executionCwd: effectiveExecutionCwd,
462
+ });
463
+ const restoreRemoteWorkspace = preparedExecutionTargetRuntime
464
+ ? () => preparedExecutionTargetRuntime.restoreWorkspace((line) => onLog("stdout", line))
465
+ : null;
466
+ const effectivePromptBundleAddDir = executionTargetIsRemote
467
+ ? preparedExecutionTargetRuntime?.assetDirs.skills ??
468
+ path.posix.join(effectiveExecutionCwd, ".paperclip-runtime", "claude", "skills")
469
+ : promptBundle.addDir;
470
+ const effectiveInstructionsFilePath = promptBundle.instructionsFilePath
471
+ ? executionTargetIsRemote
472
+ ? path.posix.join(effectivePromptBundleAddDir, path.basename(promptBundle.instructionsFilePath))
473
+ : promptBundle.instructionsFilePath
474
+ : undefined;
475
+ const effectiveMcpConfigPath = executionTargetIsRemote
476
+ ? path.posix.join(preparedExecutionTargetRuntime?.assetDirs["mcp-config"] ??
477
+ path.posix.join(effectiveExecutionCwd, ".paperclip-runtime", "claude", "mcp-config"), path.basename(localMcpConfigPath))
478
+ : localMcpConfigPath;
479
+ const remoteClaudeRuntimeRoot = executionTargetIsRemote
480
+ ? preparedExecutionTargetRuntime?.runtimeRootDir ??
481
+ path.posix.join(effectiveExecutionCwd, ".paperclip-runtime", "claude")
482
+ : null;
483
+ const remoteClaudeConfigSeedDir = claudeConfigSeedDir && remoteClaudeRuntimeRoot
484
+ ? preparedExecutionTargetRuntime?.assetDirs["config-seed"] ??
485
+ path.posix.join(remoteClaudeRuntimeRoot, "config-seed")
486
+ : null;
487
+ const remoteClaudeConfigDir = useManagedRemoteClaudeConfig && remoteClaudeRuntimeRoot
488
+ ? path.posix.join(remoteClaudeRuntimeRoot, "config")
489
+ : null;
490
+ if (remoteClaudeConfigDir && remoteClaudeConfigSeedDir) {
491
+ env.CLAUDE_CONFIG_DIR = remoteClaudeConfigDir;
492
+ loggedEnv.CLAUDE_CONFIG_DIR = remoteClaudeConfigDir;
493
+ await onLog("stdout", `[paperclip] Materializing Claude auth/config into ${remoteClaudeConfigDir}.\n`);
494
+ await materializeRemoteClaudeConfig({
495
+ runId,
496
+ target: executionTarget,
497
+ remoteClaudeConfigDir,
498
+ remoteClaudeConfigSeedDir,
499
+ options: {
500
+ cwd,
501
+ env,
502
+ timeoutSec: Math.max(timeoutSec, 15),
503
+ graceSec,
504
+ onLog,
505
+ },
506
+ });
507
+ }
508
+ let paperclipBridge = null;
509
+ if (executionTargetIsRemote && adapterExecutionTargetUsesPaperclipBridge(runtimeExecutionTarget)) {
510
+ paperclipBridge = await startAdapterExecutionTargetPaperclipBridge({
511
+ runId,
512
+ target: runtimeExecutionTarget,
513
+ enableSandboxDuplexBridge: adapterExecutionTargetEnablesSandboxDuplexBridge(runtimeExecutionTarget),
514
+ duplexObservabilityRecorder: adapterExecutionTargetDuplexObservabilityRecorder(runtimeExecutionTarget),
515
+ runtimeRootDir: preparedExecutionTargetRuntime?.runtimeRootDir,
516
+ adapterKey: "claude",
517
+ timeoutSec,
518
+ hostApiToken: env.PAPERCLIP_API_KEY,
519
+ onLog,
520
+ });
521
+ if (paperclipBridge) {
522
+ Object.assign(env, paperclipBridge.env);
523
+ const runtimeEnv = ensurePathInEnv({ ...process.env, ...env });
524
+ loggedEnv = buildInvocationEnvForLogs(env, {
525
+ runtimeEnv,
526
+ includeRuntimeKeys: ["HOME", "CLAUDE_CONFIG_DIR"],
527
+ resolvedCommand,
528
+ });
529
+ if (remoteClaudeConfigDir) {
530
+ loggedEnv.CLAUDE_CONFIG_DIR = remoteClaudeConfigDir;
531
+ }
532
+ }
533
+ }
534
+ let effectiveEffort = effort;
535
+ if (executionTargetIsSandbox && effort) {
536
+ const supportsEffort = await claudeCommandSupportsEffortFlag({
537
+ runId,
538
+ command,
539
+ target: runtimeExecutionTarget,
540
+ cwd,
541
+ env,
542
+ timeoutSec,
543
+ graceSec,
544
+ });
545
+ if (supportsEffort === false) {
546
+ effectiveEffort = "";
547
+ await onLog("stderr", `[paperclip] Claude CLI in the environment does not advertise --effort; omitting configured effort "${effort}". Upgrade the environment CLI/image to restore reasoning-effort control.\n`);
548
+ }
549
+ }
550
+ const runtimeSessionParams = parseObject(runtime.sessionParams);
551
+ const runtimeSessionId = asString(runtimeSessionParams.sessionId, runtime.sessionId ?? "");
552
+ const runtimeSessionCwd = asString(runtimeSessionParams.cwd, "");
553
+ const runtimeRemoteExecution = parseObject(runtimeSessionParams.remoteExecution);
554
+ const runtimePromptBundleKey = asString(runtimeSessionParams.promptBundleKey, "");
555
+ const runtimeMcpServerIdentity = asString(runtimeSessionParams.mcpServerIdentity, "");
556
+ const hasMatchingPromptBundle = runtimePromptBundleKey.length === 0 || runtimePromptBundleKey === promptBundle.bundleKey;
557
+ const hasMatchingMcpServers = runtimeMcpServerIdentity.length === 0
558
+ ? runtimeMcpServers.length === 0
559
+ : runtimeMcpServerIdentity === runtimeMcpIdentity;
560
+ const isValidUuid = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(runtimeSessionId);
561
+ const canResumeSession = runtimeSessionId.length > 0 &&
562
+ isValidUuid &&
563
+ hasMatchingPromptBundle &&
564
+ hasMatchingMcpServers &&
565
+ claudeSessionCwdMatchesExecutionTarget({
566
+ runtimeSessionCwd,
567
+ effectiveExecutionCwd,
568
+ executionTargetIsRemote,
569
+ }) &&
570
+ adapterExecutionTargetSessionMatches(runtimeRemoteExecution, runtimeExecutionTarget);
571
+ const sessionId = canResumeSession ? runtimeSessionId : null;
572
+ if (runtimeSessionId && !isValidUuid) {
573
+ await onLog("stdout", `[paperclip] Claude session "${runtimeSessionId}" is not a valid UUID and will not be passed to --resume.\n`);
574
+ }
575
+ if (executionTargetIsRemote &&
576
+ runtimeSessionId &&
577
+ isValidUuid &&
578
+ !canResumeSession) {
579
+ await onLog("stdout", `[paperclip] Claude session "${runtimeSessionId}" does not match the current remote execution identity and will not be resumed in "${effectiveExecutionCwd}". Starting a fresh remote session.\n`);
580
+ }
581
+ else if (runtimeSessionId &&
582
+ isValidUuid &&
583
+ runtimeSessionCwd.length > 0 &&
584
+ path.resolve(runtimeSessionCwd) !== path.resolve(effectiveExecutionCwd)) {
585
+ await onLog("stdout", `[paperclip] Claude session "${runtimeSessionId}" does not match the current remote execution identity and will not be resumed in "${effectiveExecutionCwd}". Starting a fresh remote session.\n`);
586
+ }
587
+ else if (runtimeSessionId && isValidUuid && !canResumeSession) {
588
+ await onLog("stdout", `[paperclip] Claude session "${runtimeSessionId}" was saved for cwd "${runtimeSessionCwd}" and will not be resumed in "${effectiveExecutionCwd}".\n`);
589
+ }
590
+ if (runtimeSessionId && runtimePromptBundleKey.length > 0 && runtimePromptBundleKey !== promptBundle.bundleKey) {
591
+ await onLog("stdout", `[paperclip] Claude session "${runtimeSessionId}" was saved for prompt bundle "${runtimePromptBundleKey}" and will not be resumed with "${promptBundle.bundleKey}".\n`);
592
+ }
593
+ if (runtimeSessionId && !hasMatchingMcpServers) {
594
+ await onLog("stdout", `[paperclip] Claude session "${runtimeSessionId}" was saved with a different runtime MCP server set and will not be resumed.\n`);
595
+ }
596
+ const bootstrapPromptTemplate = asString(config.bootstrapPromptTemplate, "");
597
+ const templateData = {
598
+ agentId: agent.id,
599
+ companyId: agent.companyId,
600
+ runId,
601
+ company: { id: agent.companyId },
602
+ agent,
603
+ run: { id: runId, source: "on_demand" },
604
+ context,
605
+ };
606
+ const renderedBootstrapPrompt = !sessionId && bootstrapPromptTemplate.trim().length > 0
607
+ ? renderTemplate(bootstrapPromptTemplate, templateData).trim()
608
+ : "";
609
+ const taskContextNote = selectPaperclipTaskMarkdown(context, { resumedSession: Boolean(sessionId) });
610
+ const wakePrompt = renderPaperclipWakePrompt(context.paperclipWake, {
611
+ resumedSession: Boolean(sessionId),
612
+ conversationMode: context.conversationMode === true,
613
+ // The task-context markdown is the authoritative brief on this lane; keep
614
+ // the wake prompt's description copy out so the prompt carries it once.
615
+ suppressIssueDescription: taskContextNote.length > 0,
616
+ });
617
+ const shouldUseResumeDeltaPrompt = Boolean(sessionId) && wakePrompt.length > 0;
618
+ const renderedPrompt = shouldUseResumeDeltaPrompt || isPaperclipRecoveryWakePayload(context.paperclipWake)
619
+ ? ""
620
+ : renderTemplate(promptTemplate, templateData);
621
+ const sessionHandoffNote = asString(context.paperclipSessionHandoffMarkdown, "").trim();
622
+ const prompt = joinPromptSections([
623
+ renderedBootstrapPrompt,
624
+ wakePrompt,
625
+ sessionHandoffNote,
626
+ taskContextNote,
627
+ renderedPrompt,
628
+ ]);
629
+ const promptMetrics = {
630
+ promptChars: prompt.length,
631
+ bootstrapPromptChars: renderedBootstrapPrompt.length,
632
+ wakePromptChars: wakePrompt.length,
633
+ sessionHandoffChars: sessionHandoffNote.length,
634
+ taskContextChars: taskContextNote.length,
635
+ heartbeatPromptChars: renderedPrompt.length,
636
+ };
637
+ const passesConfiguredModel = Boolean(model && (!isBedrockAuth(modelEnv) || isBedrockModelId(model)));
638
+ const buildClaudeArgs = (resumeSessionId, attemptInstructionsFilePath) => {
639
+ const args = ["--print", "--output-format", "stream-json", "--verbose"];
640
+ if (config.managedAiConnection)
641
+ args.push("--setting-sources", "user");
642
+ if (resumeSessionId)
643
+ args.push("--resume", resumeSessionId);
644
+ args.push(...buildClaudeExecutionPermissionArgs({
645
+ dangerouslySkipPermissions,
646
+ targetIsRemote: executionTargetIsRemote,
647
+ localProcessUid: process.getuid?.() ?? null,
648
+ }));
649
+ if (chrome)
650
+ args.push("--chrome");
651
+ // For Bedrock: only pass --model when the ID is a Bedrock-native identifier
652
+ // (e.g. "us.anthropic.*" or ARN). Anthropic-style IDs like "claude-opus-4-6" are invalid
653
+ // on Bedrock, so skip them and let the CLI use its own configured model.
654
+ if (passesConfiguredModel) {
655
+ args.push("--model", model);
656
+ }
657
+ if (effectiveEffort)
658
+ args.push("--effort", effectiveEffort);
659
+ if (maxTurns > 0)
660
+ args.push("--max-turns", String(maxTurns));
661
+ // On resumed sessions the instructions are already in the session cache;
662
+ // re-injecting them via --append-system-prompt-file wastes 5-10K tokens
663
+ // per heartbeat and the Claude CLI may reject the combination outright.
664
+ if (attemptInstructionsFilePath && !resumeSessionId) {
665
+ args.push("--append-system-prompt-file", attemptInstructionsFilePath);
666
+ }
667
+ if (runtimeMcpServers.length > 0) {
668
+ args.push("--mcp-config", effectiveMcpConfigPath, "--strict-mcp-config");
669
+ }
670
+ args.push("--add-dir", effectivePromptBundleAddDir);
671
+ if (extraArgs.length > 0)
672
+ args.push(...extraArgs);
673
+ return args;
674
+ };
675
+ const parseFallbackErrorMessage = (proc) => {
676
+ const stderrLine = proc.stderr
677
+ .split(/\r?\n/)
678
+ .map((line) => line.trim())
679
+ .find(Boolean) ?? "";
680
+ if ((proc.exitCode ?? 0) === 0) {
681
+ return "Failed to parse claude JSON output";
682
+ }
683
+ return stderrLine
684
+ ? `Claude exited with code ${proc.exitCode ?? -1}: ${stderrLine}`
685
+ : `Claude exited with code ${proc.exitCode ?? -1}`;
686
+ };
687
+ const runAttempt = async (resumeSessionId) => {
688
+ const attemptInstructionsFilePath = resumeSessionId ? undefined : effectiveInstructionsFilePath;
689
+ const args = buildClaudeArgs(resumeSessionId, attemptInstructionsFilePath);
690
+ const commandNotes = [];
691
+ if (!resumeSessionId) {
692
+ commandNotes.push(`Using stable Claude prompt bundle ${promptBundle.bundleKey}.`);
693
+ }
694
+ if (dangerouslySkipPermissions && executionTargetIsRemote) {
695
+ commandNotes.push("Using full Claude permission bypass for remote execution, including connected tools.");
696
+ }
697
+ if (attemptInstructionsFilePath && !resumeSessionId) {
698
+ commandNotes.push(`Injected agent instructions via --append-system-prompt-file ${instructionsFilePath} (with path directive appended)`);
699
+ }
700
+ if (runtimeMcpServers.length > 0) {
701
+ commandNotes.push(`Using ${runtimeMcpServers.length} Paperclip-managed MCP server(s) from strict config ${effectiveMcpConfigPath}.`);
702
+ }
703
+ if (onMeta) {
704
+ await onMeta({
705
+ adapterType: "claude_local",
706
+ command: resolvedCommand,
707
+ cwd: effectiveExecutionCwd,
708
+ commandArgs: args,
709
+ commandNotes,
710
+ env: loggedEnv,
711
+ prompt,
712
+ promptMetrics,
713
+ context,
714
+ });
715
+ }
716
+ const proc = await runAdapterExecutionTargetProcess(runId, runtimeExecutionTarget, command, args, {
717
+ cwd,
718
+ env,
719
+ stdin: prompt,
720
+ timeoutSec,
721
+ graceSec,
722
+ onSpawn,
723
+ onRuntimeProgress: ctx.onRuntimeProgress,
724
+ onLog,
725
+ runLogTail: paperclipBridge?.runLogTail,
726
+ settleRunDisposition: paperclipBridge?.settleRunDisposition,
727
+ terminalResultCleanup: {
728
+ graceMs: terminalResultCleanupGraceMs,
729
+ hasTerminalResult: ({ stdout }) => parseClaudeStreamJson(stdout).resultJson !== null,
730
+ },
731
+ localProcessSandbox,
732
+ });
733
+ const parsedStream = parseClaudeStreamJson(proc.stdout);
734
+ const parsed = parsedStream.resultJson ?? parseJson(proc.stdout);
735
+ return { proc, parsedStream, parsed };
736
+ };
737
+ const toAdapterResult = (attempt, opts) => {
738
+ const { proc, parsedStream, parsed } = attempt;
739
+ const loginMeta = detectClaudeLoginRequired({
740
+ parsed,
741
+ stdout: proc.stdout,
742
+ stderr: proc.stderr,
743
+ });
744
+ const errorMeta = loginMeta.loginUrl != null
745
+ ? {
746
+ loginUrl: loginMeta.loginUrl,
747
+ }
748
+ : undefined;
749
+ if (proc.timedOut) {
750
+ return {
751
+ exitCode: proc.exitCode,
752
+ signal: proc.signal,
753
+ timedOut: true,
754
+ errorMessage: `Timed out after ${timeoutSec}s`,
755
+ errorCode: "timeout",
756
+ errorMeta,
757
+ clearSession: Boolean(opts.clearSessionOnMissingSession),
758
+ };
759
+ }
760
+ if (!parsed) {
761
+ const fallbackErrorMessage = parseFallbackErrorMessage(proc);
762
+ const providerQuota = !loginMeta.requiresLogin &&
763
+ (proc.exitCode ?? 0) !== 0 &&
764
+ isClaudeProviderQuotaError({
765
+ parsed: null,
766
+ stdout: proc.stdout,
767
+ stderr: proc.stderr,
768
+ errorMessage: fallbackErrorMessage,
769
+ });
770
+ const transientUpstream = !loginMeta.requiresLogin &&
771
+ !providerQuota &&
772
+ (proc.exitCode ?? 0) !== 0 &&
773
+ isClaudeTransientUpstreamError({
774
+ parsed: null,
775
+ stdout: proc.stdout,
776
+ stderr: proc.stderr,
777
+ errorMessage: fallbackErrorMessage,
778
+ });
779
+ const transientRetryNotBefore = providerQuota || transientUpstream
780
+ ? extractClaudeRetryNotBefore({
781
+ parsed: null,
782
+ stdout: proc.stdout,
783
+ stderr: proc.stderr,
784
+ errorMessage: fallbackErrorMessage,
785
+ })
786
+ : null;
787
+ const errorCode = proc.errorCode
788
+ // Forward the transport-level error code from the run-disposition seam
789
+ // first, even on the unparsed path. A lost duplex control channel
790
+ // surfaces the typed `duplex_channel_lost` code before any provider
791
+ // classification, so the CLI lane and the ACP lane report it alike.
792
+ ? proc.errorCode
793
+ : loginMeta.requiresLogin
794
+ ? "claude_auth_required"
795
+ : isClaudeModelNotFoundError({
796
+ parsed: null,
797
+ stdout: proc.stdout,
798
+ stderr: proc.stderr,
799
+ errorMessage: fallbackErrorMessage,
800
+ })
801
+ ? "model_not_found"
802
+ : providerQuota
803
+ ? "provider_quota"
804
+ : transientUpstream
805
+ ? "claude_transient_upstream"
806
+ : null;
807
+ const errorFamily = providerQuota ? "provider_quota" : transientUpstream ? "transient_upstream" : null;
808
+ return {
809
+ exitCode: proc.exitCode,
810
+ signal: proc.signal,
811
+ timedOut: false,
812
+ errorMessage: fallbackErrorMessage,
813
+ errorCode,
814
+ errorFamily,
815
+ retryNotBefore: transientRetryNotBefore ? transientRetryNotBefore.toISOString() : null,
816
+ errorMeta,
817
+ resultJson: {
818
+ stdout: proc.stdout,
819
+ stderr: proc.stderr,
820
+ ...(errorFamily ? { errorFamily } : {}),
821
+ ...(transientRetryNotBefore
822
+ ? { retryNotBefore: transientRetryNotBefore.toISOString() }
823
+ : {}),
824
+ ...(transientRetryNotBefore
825
+ ? { transientRetryNotBefore: transientRetryNotBefore.toISOString() }
826
+ : {}),
827
+ ...(providerQuota && transientRetryNotBefore
828
+ ? { providerQuotaRetryNotBefore: transientRetryNotBefore.toISOString() }
829
+ : {}),
830
+ ...(proc.terminalResultCleanup ? { unmanagedBackgroundTask: proc.terminalResultCleanup } : {}),
831
+ },
832
+ clearSession: Boolean(opts.clearSessionOnMissingSession),
833
+ };
834
+ }
835
+ const fallbackModelUsageTotals = parsedStream.usage ? null : claudeModelUsageTotals(parsed.modelUsage);
836
+ const usage = parsedStream.usage ??
837
+ fallbackModelUsageTotals ??
838
+ (() => {
839
+ const usageObj = parseObject(parsed.usage);
840
+ return {
841
+ inputTokens: asNumber(usageObj.input_tokens, 0),
842
+ cachedInputTokens: asNumber(usageObj.cache_read_input_tokens, 0),
843
+ outputTokens: asNumber(usageObj.output_tokens, 0),
844
+ };
845
+ })();
846
+ const usageBasis = parsedStream.usage
847
+ ? parsedStream.usageBasis
848
+ : fallbackModelUsageTotals
849
+ ? "per_run"
850
+ : null;
851
+ const rawResolvedSessionId = parsedStream.sessionId ??
852
+ (asString(parsed.session_id, opts.fallbackSessionId ?? "") || opts.fallbackSessionId);
853
+ const clearSessionForMaxTurns = isClaudeMaxTurnsResult(parsed);
854
+ const poisonedPreviousMessageId = isClaudePoisonedPreviousMessageIdError(parsed);
855
+ // Fable 5 policy refusals exit cleanly (exitCode=0, is_error=false), so this
856
+ // is intentionally independent of `failed` — otherwise a refusal looks like a
857
+ // successful run to Paperclip and the heartbeat stalls silently. See RY-604.
858
+ const claudeRefusal = isClaudeRefusalResult(parsed);
859
+ const parsedIsError = asBoolean(parsed.is_error, false);
860
+ const parsedSubtype = asString(parsed.subtype, "").trim().toLowerCase();
861
+ const parsedSucceeded = parsedSubtype === "success" && !parsedIsError;
862
+ const failed = !parsedSucceeded && ((proc.exitCode ?? 0) !== 0 || parsedIsError);
863
+ // Validate-before-persist guard: never persist a sessionId whose transcript
864
+ // is known-poisoned. The Claude CLI keeps an on-disk JSONL keyed by the
865
+ // session id; if the last entry contains a non-`msg_`-prefixed
866
+ // `previous_message_id`, every subsequent `--resume` hits a 400 from
867
+ // /v1/messages and the issue is permanently unrecoverable until the
868
+ // sessionId is dropped server-side. Drop here so resolveNextSessionState
869
+ // calls clearTaskSessions on the next heartbeat. See RED-978 / RED-976.
870
+ const shouldDropSessionForPoison = poisonedPreviousMessageId;
871
+ const resolvedSessionId = shouldDropSessionForPoison ? null : rawResolvedSessionId;
872
+ const resolvedSessionParams = resolvedSessionId
873
+ ? {
874
+ sessionId: resolvedSessionId,
875
+ cwd,
876
+ promptBundleKey: promptBundle.bundleKey,
877
+ mcpServerIdentity: runtimeMcpIdentity,
878
+ ...(executionTargetIsRemote
879
+ ? {
880
+ remoteExecution: adapterExecutionTargetSessionIdentity(runtimeExecutionTarget),
881
+ }
882
+ : {}),
883
+ ...(workspaceId ? { workspaceId } : {}),
884
+ ...(workspaceRepoUrl ? { repoUrl: workspaceRepoUrl } : {}),
885
+ ...(workspaceRepoRef ? { repoRef: workspaceRepoRef } : {}),
886
+ }
887
+ : null;
888
+ const errorMessage = failed
889
+ ? describeClaudeFailure(parsed) ?? `Claude exited with code ${proc.exitCode ?? -1}`
890
+ : null;
891
+ const providerQuota = failed &&
892
+ !loginMeta.requiresLogin &&
893
+ !clearSessionForMaxTurns &&
894
+ !poisonedPreviousMessageId &&
895
+ isClaudeProviderQuotaError({
896
+ parsed,
897
+ stdout: proc.stdout,
898
+ stderr: proc.stderr,
899
+ errorMessage,
900
+ });
901
+ const transientUpstream = failed &&
902
+ !loginMeta.requiresLogin &&
903
+ !clearSessionForMaxTurns &&
904
+ !poisonedPreviousMessageId &&
905
+ !providerQuota &&
906
+ isClaudeTransientUpstreamError({
907
+ parsed,
908
+ stdout: proc.stdout,
909
+ stderr: proc.stderr,
910
+ errorMessage,
911
+ });
912
+ const transientRetryNotBefore = providerQuota || transientUpstream
913
+ ? extractClaudeRetryNotBefore({
914
+ parsed,
915
+ stdout: proc.stdout,
916
+ stderr: proc.stderr,
917
+ errorMessage,
918
+ })
919
+ : null;
920
+ const resolvedErrorCode = proc.errorCode
921
+ // Forward the transport-level error code from the run-disposition seam
922
+ // first. A lost duplex control channel surfaces the typed
923
+ // `duplex_channel_lost` code before any provider classification.
924
+ ? proc.errorCode
925
+ : loginMeta.requiresLogin
926
+ ? "claude_auth_required"
927
+ : failed && isClaudeModelNotFoundError({
928
+ parsed,
929
+ stdout: proc.stdout,
930
+ stderr: proc.stderr,
931
+ errorMessage,
932
+ })
933
+ ? "model_not_found"
934
+ : failed && clearSessionForMaxTurns
935
+ ? "max_turns_exhausted"
936
+ : failed && poisonedPreviousMessageId
937
+ ? "claude_poisoned_previous_message_id"
938
+ : providerQuota
939
+ ? "provider_quota"
940
+ : transientUpstream
941
+ ? "claude_transient_upstream"
942
+ : claudeRefusal
943
+ ? "claude_refusal"
944
+ : null;
945
+ const errorFamily = providerQuota
946
+ ? "provider_quota"
947
+ : transientUpstream
948
+ ? "transient_upstream"
949
+ : claudeRefusal
950
+ ? "model_refusal"
951
+ : null;
952
+ const mergedResultJson = {
953
+ ...parsed,
954
+ ...(failed && clearSessionForMaxTurns ? { stopReason: "max_turns_exhausted" } : {}),
955
+ ...(failed && poisonedPreviousMessageId ? { stopReason: "claude_poisoned_previous_message_id" } : {}),
956
+ ...(claudeRefusal ? { stopReason: "refusal", errorFamily: "model_refusal" } : {}),
957
+ ...(errorFamily ? { errorFamily } : {}),
958
+ ...(transientRetryNotBefore ? { retryNotBefore: transientRetryNotBefore.toISOString() } : {}),
959
+ ...(transientRetryNotBefore ? { transientRetryNotBefore: transientRetryNotBefore.toISOString() } : {}),
960
+ ...(providerQuota && transientRetryNotBefore ? { providerQuotaRetryNotBefore: transientRetryNotBefore.toISOString() } : {}),
961
+ ...(proc.terminalResultCleanup ? { unmanagedBackgroundTask: proc.terminalResultCleanup } : {}),
962
+ };
963
+ return {
964
+ exitCode: proc.exitCode,
965
+ signal: proc.signal,
966
+ timedOut: false,
967
+ errorMessage,
968
+ errorCode: resolvedErrorCode,
969
+ errorFamily,
970
+ retryNotBefore: transientRetryNotBefore ? transientRetryNotBefore.toISOString() : null,
971
+ errorMeta,
972
+ usage,
973
+ ...(usageBasis ? { usageBasis } : {}),
974
+ sessionId: resolvedSessionId,
975
+ sessionParams: resolvedSessionParams,
976
+ sessionDisplayId: resolvedSessionId,
977
+ provider: "anthropic",
978
+ biller: isBedrockAuth(effectiveEnv) ? "aws_bedrock" : "anthropic",
979
+ model: parsedStream.model || asString(parsed.model, model),
980
+ billingType,
981
+ costUsd: parsedStream.costUsd,
982
+ resultJson: mergedResultJson,
983
+ summary: parsedStream.summary || asString(parsed.result, ""),
984
+ clearSession: clearSessionForMaxTurns ||
985
+ // Clear-on-error: a poisoned previous_message_id is a deterministic
986
+ // state error. Force the server to drop persisted session state for
987
+ // this issue so the next continuation starts from a clean slate.
988
+ poisonedPreviousMessageId ||
989
+ Boolean(opts.clearSessionOnMissingSession && !resolvedSessionId),
990
+ };
991
+ };
992
+ try {
993
+ const minimumCliVersion = passesConfiguredModel && claudeCommandLooksLike(command, "claude")
994
+ ? minimumClaudeCliVersionForModel(model)
995
+ : null;
996
+ if (minimumCliVersion) {
997
+ const detectedCliVersion = await readClaudeCommandVersion({
998
+ runId,
999
+ command,
1000
+ target: runtimeExecutionTarget,
1001
+ cwd,
1002
+ env,
1003
+ timeoutSec,
1004
+ graceSec,
1005
+ });
1006
+ if (!detectedCliVersion ||
1007
+ !claudeCliVersionAtLeast(detectedCliVersion, minimumCliVersion)) {
1008
+ const detected = detectedCliVersion
1009
+ ? `detected ${detectedCliVersion}`
1010
+ : "could not determine the installed version";
1011
+ const errorMessage = `${model} requires Claude Code ${minimumCliVersion} or newer on the CLI lane; ${detected}. ` +
1012
+ "Upgrade Claude Code or restore the default ACP lane before retrying.";
1013
+ await onLog("stderr", `[paperclip] ${errorMessage}\n`);
1014
+ return {
1015
+ exitCode: 1,
1016
+ signal: null,
1017
+ timedOut: false,
1018
+ errorMessage,
1019
+ errorCode: "claude_cli_version_incompatible",
1020
+ provider: "anthropic",
1021
+ biller: isBedrockAuth(effectiveEnv) ? "aws_bedrock" : "anthropic",
1022
+ model,
1023
+ billingType,
1024
+ resultJson: {
1025
+ stopReason: "claude_cli_version_incompatible",
1026
+ requiredClaudeCodeVersion: minimumCliVersion,
1027
+ detectedClaudeCodeVersion: detectedCliVersion,
1028
+ },
1029
+ };
1030
+ }
1031
+ }
1032
+ const initial = await runAttempt(sessionId ?? null);
1033
+ const sessionErrorKind = sessionId &&
1034
+ !initial.proc.timedOut &&
1035
+ (initial.proc.exitCode ?? 0) !== 0 &&
1036
+ initial.parsed
1037
+ ? isClaudeUnknownSessionError(initial.parsed)
1038
+ ? "unknown"
1039
+ : isClaudePoisonedPreviousMessageIdError(initial.parsed)
1040
+ ? "poisoned"
1041
+ : isClaudeImageProcessingError(initial.parsed)
1042
+ ? "image"
1043
+ : null
1044
+ : null;
1045
+ if (sessionErrorKind !== null) {
1046
+ const reason = sessionErrorKind === "poisoned"
1047
+ ? "returned a poisoned message-id"
1048
+ : sessionErrorKind === "image"
1049
+ ? "contains an unprocessable image"
1050
+ : "is unavailable";
1051
+ await onLog("stdout", `[paperclip] Claude resume session "${sessionId}" ${reason}; retrying with a fresh session.\n`);
1052
+ if (sessionErrorKind === "poisoned" && !executionTargetIsRemote) {
1053
+ const claudeConfigDir = resolveSharedClaudeConfigDir(effectiveEnv);
1054
+ // Mirrors Claude Code's project-dir encoding: non-alphanumeric chars become "-"; existing hyphens pass through.
1055
+ const encodedCwd = effectiveExecutionCwd.replace(/[^a-zA-Z0-9-]/g, "-");
1056
+ const poisonedJsonlPath = path.join(claudeConfigDir, "projects", encodedCwd, `${sessionId}.jsonl`);
1057
+ let unlinked = false;
1058
+ try {
1059
+ await fs.unlink(poisonedJsonlPath);
1060
+ unlinked = true;
1061
+ }
1062
+ catch {
1063
+ // best-effort; session is cleared server-side regardless
1064
+ }
1065
+ if (unlinked) {
1066
+ try {
1067
+ await onLog("stdout", `[paperclip] Removed poisoned session file: ${poisonedJsonlPath}\n`);
1068
+ }
1069
+ catch {
1070
+ // log stream may be closed; the unlink already succeeded
1071
+ }
1072
+ }
1073
+ }
1074
+ const retry = await runAttempt(null);
1075
+ return toAdapterResult(retry, { fallbackSessionId: null, clearSessionOnMissingSession: true });
1076
+ }
1077
+ return toAdapterResult(initial, { fallbackSessionId: runtimeSessionId || runtime.sessionId });
1078
+ }
1079
+ finally {
1080
+ if (paperclipBridge) {
1081
+ await paperclipBridge.stop();
1082
+ }
1083
+ if (restoreRemoteWorkspace) {
1084
+ await onLog("stdout", `[paperclip] Restoring workspace changes from ${describeAdapterExecutionTarget(executionTarget)}.\n`);
1085
+ await restoreRemoteWorkspace();
1086
+ }
1087
+ }
1088
+ }
1089
+ //# sourceMappingURL=execute.js.map