@tickernelz/paperclip-pro-adapter-utils 2026.1005.10 → 2026.1006.1

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 (181) hide show
  1. package/README.md +13 -0
  2. package/dist/acpx-engine/ephemeral-session-environment.d.ts +4 -0
  3. package/dist/acpx-engine/ephemeral-session-environment.d.ts.map +1 -0
  4. package/dist/acpx-engine/ephemeral-session-environment.js +29 -0
  5. package/dist/acpx-engine/ephemeral-session-environment.js.map +1 -0
  6. package/dist/acpx-engine/ephemeral-session-environment.test.d.ts +2 -0
  7. package/dist/acpx-engine/ephemeral-session-environment.test.d.ts.map +1 -0
  8. package/dist/acpx-engine/ephemeral-session-environment.test.js +177 -0
  9. package/dist/acpx-engine/ephemeral-session-environment.test.js.map +1 -0
  10. package/dist/acpx-engine/execute.d.ts +2 -6
  11. package/dist/acpx-engine/execute.d.ts.map +1 -1
  12. package/dist/acpx-engine/execute.js +125 -62
  13. package/dist/acpx-engine/execute.js.map +1 -1
  14. package/dist/acpx-engine/execute.test.js +299 -3
  15. package/dist/acpx-engine/execute.test.js.map +1 -1
  16. package/dist/acpx-engine/session-codec.d.ts.map +1 -1
  17. package/dist/acpx-engine/session-codec.js +1 -0
  18. package/dist/acpx-engine/session-codec.js.map +1 -1
  19. package/dist/acpx-engine/settlement-characterization.test.js +105 -0
  20. package/dist/acpx-engine/settlement-characterization.test.js.map +1 -1
  21. package/dist/acpx-engine/spawn-smoke.test.js +16 -7
  22. package/dist/acpx-engine/spawn-smoke.test.js.map +1 -1
  23. package/dist/acpx-engine/startup-cancellation.d.ts +1 -0
  24. package/dist/acpx-engine/startup-cancellation.d.ts.map +1 -1
  25. package/dist/acpx-engine/startup-cancellation.js +7 -2
  26. package/dist/acpx-engine/startup-cancellation.js.map +1 -1
  27. package/dist/acpx-engine/terminal-session-failure.d.ts +18 -0
  28. package/dist/acpx-engine/terminal-session-failure.d.ts.map +1 -0
  29. package/dist/acpx-engine/terminal-session-failure.js +106 -0
  30. package/dist/acpx-engine/terminal-session-failure.js.map +1 -0
  31. package/dist/acpx-engine/terminal-session-failure.test.d.ts +2 -0
  32. package/dist/acpx-engine/terminal-session-failure.test.d.ts.map +1 -0
  33. package/dist/acpx-engine/terminal-session-failure.test.js +139 -0
  34. package/dist/acpx-engine/terminal-session-failure.test.js.map +1 -0
  35. package/dist/acpx-engine/turn-characterization.test.js +4 -3
  36. package/dist/acpx-engine/turn-characterization.test.js.map +1 -1
  37. package/dist/command-managed-runtime.d.ts +2 -0
  38. package/dist/command-managed-runtime.d.ts.map +1 -1
  39. package/dist/command-managed-runtime.js +2 -0
  40. package/dist/command-managed-runtime.js.map +1 -1
  41. package/dist/command-redaction.d.ts +2 -0
  42. package/dist/command-redaction.d.ts.map +1 -1
  43. package/dist/command-redaction.js +20 -10
  44. package/dist/command-redaction.js.map +1 -1
  45. package/dist/command-redaction.test.js +20 -1
  46. package/dist/command-redaction.test.js.map +1 -1
  47. package/dist/directory-merge-lock.test.d.ts +2 -0
  48. package/dist/directory-merge-lock.test.d.ts.map +1 -0
  49. package/dist/directory-merge-lock.test.js +166 -0
  50. package/dist/directory-merge-lock.test.js.map +1 -0
  51. package/dist/execution-phase.d.ts +10 -0
  52. package/dist/execution-phase.d.ts.map +1 -0
  53. package/dist/execution-phase.js +27 -0
  54. package/dist/execution-phase.js.map +1 -0
  55. package/dist/execution-phase.test.d.ts +2 -0
  56. package/dist/execution-phase.test.d.ts.map +1 -0
  57. package/dist/execution-phase.test.js +26 -0
  58. package/dist/execution-phase.test.js.map +1 -0
  59. package/dist/execution-target-sandbox.test.js +49 -7
  60. package/dist/execution-target-sandbox.test.js.map +1 -1
  61. package/dist/execution-target-stdin-race.test.js +245 -2
  62. package/dist/execution-target-stdin-race.test.js.map +1 -1
  63. package/dist/execution-target.d.ts +6 -0
  64. package/dist/execution-target.d.ts.map +1 -1
  65. package/dist/execution-target.js +74 -22
  66. package/dist/execution-target.js.map +1 -1
  67. package/dist/git-workspace-sync.d.ts +15 -5
  68. package/dist/git-workspace-sync.d.ts.map +1 -1
  69. package/dist/git-workspace-sync.js +267 -124
  70. package/dist/git-workspace-sync.js.map +1 -1
  71. package/dist/git-workspace-sync.test.js +296 -20
  72. package/dist/git-workspace-sync.test.js.map +1 -1
  73. package/dist/github-launcher.d.ts.map +1 -1
  74. package/dist/github-launcher.js +26 -11
  75. package/dist/github-launcher.js.map +1 -1
  76. package/dist/github-launcher.test.js +34 -1
  77. package/dist/github-launcher.test.js.map +1 -1
  78. package/dist/index.d.ts +1 -1
  79. package/dist/index.d.ts.map +1 -1
  80. package/dist/index.js +1 -1
  81. package/dist/index.js.map +1 -1
  82. package/dist/paperclip-runner-permissions.d.ts +28 -1
  83. package/dist/paperclip-runner-permissions.d.ts.map +1 -1
  84. package/dist/paperclip-runner-permissions.js +9 -1
  85. package/dist/paperclip-runner-permissions.js.map +1 -1
  86. package/dist/prompt-sections.test.d.ts +2 -0
  87. package/dist/prompt-sections.test.d.ts.map +1 -0
  88. package/dist/prompt-sections.test.js +53 -0
  89. package/dist/prompt-sections.test.js.map +1 -0
  90. package/dist/provider-stopped-boundary.d.ts +7 -0
  91. package/dist/provider-stopped-boundary.d.ts.map +1 -0
  92. package/dist/provider-stopped-boundary.js +22 -0
  93. package/dist/provider-stopped-boundary.js.map +1 -0
  94. package/dist/provider-stopped-boundary.test.d.ts +2 -0
  95. package/dist/provider-stopped-boundary.test.d.ts.map +1 -0
  96. package/dist/provider-stopped-boundary.test.js +76 -0
  97. package/dist/provider-stopped-boundary.test.js.map +1 -0
  98. package/dist/remote-managed-runtime.d.ts +2 -0
  99. package/dist/remote-managed-runtime.d.ts.map +1 -1
  100. package/dist/remote-managed-runtime.js +3 -1
  101. package/dist/remote-managed-runtime.js.map +1 -1
  102. package/dist/remote-managed-runtime.test.js +25 -0
  103. package/dist/remote-managed-runtime.test.js.map +1 -1
  104. package/dist/sandbox-callback-bridge.d.ts +2 -0
  105. package/dist/sandbox-callback-bridge.d.ts.map +1 -1
  106. package/dist/sandbox-callback-bridge.js +34 -10
  107. package/dist/sandbox-callback-bridge.js.map +1 -1
  108. package/dist/sandbox-callback-bridge.test.js +106 -3
  109. package/dist/sandbox-callback-bridge.test.js.map +1 -1
  110. package/dist/sandbox-managed-runtime.d.ts +3 -0
  111. package/dist/sandbox-managed-runtime.d.ts.map +1 -1
  112. package/dist/sandbox-managed-runtime.js +749 -718
  113. package/dist/sandbox-managed-runtime.js.map +1 -1
  114. package/dist/sandbox-managed-runtime.test.js +171 -2
  115. package/dist/sandbox-managed-runtime.test.js.map +1 -1
  116. package/dist/server-utils.d.ts +43 -1
  117. package/dist/server-utils.d.ts.map +1 -1
  118. package/dist/server-utils.js +103 -31
  119. package/dist/server-utils.js.map +1 -1
  120. package/dist/server-utils.test.js +235 -45
  121. package/dist/server-utils.test.js.map +1 -1
  122. package/dist/session-compaction.d.ts.map +1 -1
  123. package/dist/session-compaction.js +6 -0
  124. package/dist/session-compaction.js.map +1 -1
  125. package/dist/ssh-fixture.test.js +22 -0
  126. package/dist/ssh-fixture.test.js.map +1 -1
  127. package/dist/ssh.d.ts +2 -0
  128. package/dist/ssh.d.ts.map +1 -1
  129. package/dist/ssh.js +2 -2
  130. package/dist/ssh.js.map +1 -1
  131. package/dist/test-fixtures/prompt-context.d.ts +54 -0
  132. package/dist/test-fixtures/prompt-context.d.ts.map +1 -0
  133. package/dist/test-fixtures/prompt-context.js +55 -0
  134. package/dist/test-fixtures/prompt-context.js.map +1 -0
  135. package/dist/types.d.ts +12 -2
  136. package/dist/types.d.ts.map +1 -1
  137. package/dist/workspace-git-stream.d.ts +19 -0
  138. package/dist/workspace-git-stream.d.ts.map +1 -0
  139. package/dist/workspace-git-stream.js +99 -0
  140. package/dist/workspace-git-stream.js.map +1 -0
  141. package/dist/workspace-git-stream.test.d.ts +2 -0
  142. package/dist/workspace-git-stream.test.d.ts.map +1 -0
  143. package/dist/workspace-git-stream.test.js +71 -0
  144. package/dist/workspace-git-stream.test.js.map +1 -0
  145. package/dist/workspace-manifest.d.ts +61 -0
  146. package/dist/workspace-manifest.d.ts.map +1 -0
  147. package/dist/workspace-manifest.js +257 -0
  148. package/dist/workspace-manifest.js.map +1 -0
  149. package/dist/workspace-manifest.test.d.ts +2 -0
  150. package/dist/workspace-manifest.test.d.ts.map +1 -0
  151. package/dist/workspace-manifest.test.js +160 -0
  152. package/dist/workspace-manifest.test.js.map +1 -0
  153. package/dist/workspace-restore-diagnostics.d.ts +34 -0
  154. package/dist/workspace-restore-diagnostics.d.ts.map +1 -0
  155. package/dist/workspace-restore-diagnostics.js +238 -0
  156. package/dist/workspace-restore-diagnostics.js.map +1 -0
  157. package/dist/workspace-restore-diagnostics.test.d.ts +2 -0
  158. package/dist/workspace-restore-diagnostics.test.d.ts.map +1 -0
  159. package/dist/workspace-restore-diagnostics.test.js +258 -0
  160. package/dist/workspace-restore-diagnostics.test.js.map +1 -0
  161. package/dist/workspace-restore-merge.d.ts +47 -6
  162. package/dist/workspace-restore-merge.d.ts.map +1 -1
  163. package/dist/workspace-restore-merge.js +382 -114
  164. package/dist/workspace-restore-merge.js.map +1 -1
  165. package/dist/workspace-restore-merge.test.js +253 -4
  166. package/dist/workspace-restore-merge.test.js.map +1 -1
  167. package/dist/workspace-restore-result.d.ts +6 -0
  168. package/dist/workspace-restore-result.d.ts.map +1 -0
  169. package/dist/workspace-restore-result.js +72 -0
  170. package/dist/workspace-restore-result.js.map +1 -0
  171. package/dist/workspace-restore-result.test.d.ts +2 -0
  172. package/dist/workspace-restore-result.test.d.ts.map +1 -0
  173. package/dist/workspace-restore-result.test.js +74 -0
  174. package/dist/workspace-restore-result.test.js.map +1 -0
  175. package/dist/workspace-restore-teardown.d.ts.map +1 -1
  176. package/dist/workspace-restore-teardown.js +5 -3
  177. package/dist/workspace-restore-teardown.js.map +1 -1
  178. package/dist/workspace-restore-teardown.test.js +24 -0
  179. package/dist/workspace-restore-teardown.test.js.map +1 -1
  180. package/node_modules/acpx/dist/live-checkpoint-ClPCSdrW.js +4 -3
  181. package/package.json +2 -2
@@ -1,4 +1,5 @@
1
1
  import { cancellableSandboxStartup } from "./startup-cancellation.js";
2
+ import { withAdapterExecutionPhase } from "../execution-phase.js";
2
3
  import fs from "node:fs/promises";
3
4
  import fsSync from "node:fs";
4
5
  import os from "node:os";
@@ -6,16 +7,19 @@ import path from "node:path";
6
7
  import { execFile } from "node:child_process";
7
8
  import { promisify } from "node:util";
8
9
  import { createHash, randomUUID } from "node:crypto";
10
+ import { parseFrontmatterMarkdown } from "@tickernelz/paperclip-pro-shared";
9
11
  import { fileURLToPath } from "node:url";
10
12
  import { adapterExecutionTargetSessionIdentity, describeAdapterExecutionTarget, adapterExecutionTargetDuplexObservabilityRecorder, adapterExecutionTargetEnablesSandboxDuplexBridge, formatAdapterExecutionTimeoutErrorMessage, formatAdapterExecutionTimeoutStartLogLine, prepareAdapterExecutionTargetRuntime, readAdapterExecutionTarget, resolveAdapterExecutionTargetTimeout, resolveReferencedSourceIgnore, runAdapterExecutionTargetShellCommand, startAdapterExecutionTargetPaperclipBridge, startAdapterExecutionTargetProcessSessionBridge, } from "@tickernelz/paperclip-pro-adapter-utils/execution-target";
11
13
  import { captureLocalProcess, capturedProcessExited, killCapturedLocalProcess } from "./local-process-control.js";
12
14
  import { DUPLEX_CHANNEL_LOST_ERROR_CODE } from "../bridge-transport-contract.js";
15
+ import { classifyToolDefinitionFailure, formatTerminalSessionFailure, sanitizeTerminalSessionFailure, } from "./terminal-session-failure.js";
13
16
  import { classifyWorkspaceRestoreFailure, describeWorkspaceRestoreFailure, } from "../workspace-restore-merge.js";
14
- import { paperclipAgentPromptTemplate, DEFAULT_PAPERCLIP_CONVERSATION_PROMPT_TEMPLATE, applyPaperclipWorkspaceEnv, asNumber, asString, buildInvocationEnvForLogs, buildPaperclipEnv, ensureAbsoluteDirectory, ensurePathInEnv, ensurePaperclipSkillSymlink, isForbiddenConfigEnvKey, isPaperclipExternalChatTurn, isPaperclipRuntimeEnvKey, joinPromptSections, materializePaperclipSkillCopy, parseObject, isPaperclipSkillSourceMissing, readPaperclipRuntimeSkillEntries, readPaperclipIssueWorkModeFromContext, renderPaperclipWakePrompt, renderTemplate, resolvePaperclipInstanceRootForAdapter, selectPaperclipTaskMarkdown, resolveLegacyPaperclipDesiredSkillNames, removeMaintainerOnlySkillSymlinks, rewriteWorkspaceCwdEnvVarsForExecution, shapePaperclipWorkspaceEnvForExecution, } from "@tickernelz/paperclip-pro-adapter-utils/server-utils";
17
+ import { paperclipAgentPromptTemplate, DEFAULT_PAPERCLIP_CONVERSATION_PROMPT_TEMPLATE, applyPaperclipWorkspaceEnv, asNumber, asString, buildInvocationEnvForLogs, buildPaperclipEnv, ensureAbsoluteDirectory, ensurePathInEnv, ensurePaperclipSkillSymlink, isForbiddenConfigEnvKey, isPaperclipExternalChatTurn, isPaperclipRuntimeEnvKey, joinPromptSections, materializePaperclipSkillCopy, parseObject, isPaperclipSkillSourceMissing, readPaperclipRuntimeSkillEntries, readPaperclipIssueWorkModeFromContext, renderTemplate, resolvePaperclipInstanceRootForAdapter, hydrateFreshSessionHandoff, selectInitialCommunicationGuidance, selectPaperclipPromptSections, resolveLegacyPaperclipDesiredSkillNames, removeMaintainerOnlySkillSymlinks, rewriteWorkspaceCwdEnvVarsForExecution, shapePaperclipWorkspaceEnvForExecution, } from "@tickernelz/paperclip-pro-adapter-utils/server-utils";
15
18
  import { PAPERCLIP_MCP_SERVER_NAME, paperclipAccessGuidance, paperclipAccessMode, paperclipMcpToolsets, } from "@tickernelz/paperclip-pro-adapter-utils/paperclip-mcp";
16
19
  import { paperclipMcpHttpTarget } from "@tickernelz/paperclip-pro-adapter-utils/paperclip-mcp-mount";
17
20
  import { shellQuote } from "@tickernelz/paperclip-pro-adapter-utils/ssh";
18
21
  import { runToolProfile } from "@tickernelz/paperclip-pro-adapter-utils/tool-profile";
22
+ import { createEphemeralSessionEnvironmentStore } from "./ephemeral-session-environment.js";
19
23
  import { createAcpRuntime, createAgentRegistry, createRuntimeStore, isAcpRuntimeError, } from "acpx/runtime";
20
24
  import { ACPX_DUPLEX_LOSS_CANCEL_DEADLINE_MS, ACPX_HANDSHAKE_TIMEOUT_MS, ACPX_HANDSHAKE_TRANSPORT_POLL_MS, DEFAULT_ACP_ENGINE_AGENT, DEFAULT_ACP_ENGINE_MODE, DEFAULT_ACP_ENGINE_NON_INTERACTIVE_PERMISSIONS, DEFAULT_ACP_ENGINE_PERMISSION_MODE, DEFAULT_ACP_ENGINE_TIMEOUT_SEC, DEFAULT_ACP_ENGINE_WARM_HANDLE_IDLE_MS, } from "./constants.js";
21
25
  import { createRunResourceLedger } from "./run-resource-ledger.js";
@@ -337,6 +341,9 @@ function isCodexCredentialStoreHome(homePath) {
337
341
  const segments = resolved.slice(companiesRoot.length + 1).split(path.sep);
338
342
  return segments.length >= 2 && segments[1] === "codex-auth-cache";
339
343
  }
344
+ function resolveManagedCodexApiKeyHomeDir(companyId, agentId) {
345
+ return path.join(defaultStateDir(companyId, agentId), "codex-home");
346
+ }
340
347
  // Mirrors `resolveManagedGrokHomeDir` in
341
348
  // `packages/adapters/grok-local/src/server/grok-home.ts` — this package
342
349
  // cannot import that adapter package (it would invert the dependency
@@ -627,11 +634,25 @@ async function prepareClaudeSkillRuntime(input) {
627
634
  }
628
635
  }
629
636
  const selectedNames = materializedNames.sort();
637
+ const skillDescriptions = await Promise.all(selectedNames.map(async (name) => {
638
+ const file = path.join(skillsHome, name, "SKILL.md");
639
+ let description = "";
640
+ try {
641
+ const parsed = parseFrontmatterMarkdown(await fs.readFile(file, "utf8"));
642
+ description = asString(parsed.frontmatter.description, "").replace(/\s+/g, " ").trim().slice(0, 512);
643
+ }
644
+ catch {
645
+ // A readable skill without valid routing metadata remains available.
646
+ // Do not substitute its body for a missing description.
647
+ }
648
+ return `- ${name}${description ? `: ${description}` : ""} (file: ${file})`;
649
+ }));
630
650
  const promptInstructions = selectedNames.length > 0
631
651
  ? [
632
652
  "Paperclip has materialized selected runtime skills for this ACPX Claude session.",
633
653
  `Skill root: ${skillsHome}`,
634
654
  `Selected skills: ${selectedNames.join(", ")}`,
655
+ ...skillDescriptions,
635
656
  "When a task calls for one of these skills, read its SKILL.md from that root and follow it.",
636
657
  ].join("\n")
637
658
  : "";
@@ -725,8 +746,10 @@ async function prepareCodexSkillRuntime(input) {
725
746
  : process.env.OPENAI_API_KEY?.trim() || process.env.CODEX_API_KEY?.trim();
726
747
  const apiKey = input.env.OPENAI_API_KEY?.trim() || input.env.CODEX_API_KEY?.trim()
727
748
  || hostApiKey;
749
+ // Keep API-key auth separate from the company home used by subscription
750
+ // agents, so one agent cannot switch another agent's login on its next turn.
728
751
  const managedCodexHome = apiKey
729
- ? path.join(defaultStateDir(input.companyId, input.agentId), "codex-home")
752
+ ? resolveManagedCodexApiKeyHomeDir(input.companyId, input.agentId)
730
753
  : resolveManagedCodexHomeDir(input.companyId);
731
754
  const effectiveCodexHome = configuredCodexHome ??
732
755
  await prepareManagedCodexHome({
@@ -945,6 +968,7 @@ function buildSessionParams(input) {
945
968
  mode: prepared.mode,
946
969
  stateDir: prepared.stateDir,
947
970
  configFingerprint: prepared.fingerprint,
971
+ mcpFingerprint: shortHash(prepared.mcpIdentity),
948
972
  ...(prepared.requestedModel ? { model: prepared.requestedModel } : {}),
949
973
  ...(prepared.requestedThinkingEffort ? { thinkingEffort: prepared.requestedThinkingEffort } : {}),
950
974
  ...(prepared.fastMode ? { fastMode: true } : {}),
@@ -1379,7 +1403,11 @@ async function buildRuntime(input) {
1379
1403
  paperclipClaudeSettings = await writePaperclipClaudeSettings({
1380
1404
  cwd,
1381
1405
  stateDir,
1382
- agentHome,
1406
+ // A run's writable AGENT_HOME moves, but the existing permission root
1407
+ // must not invalidate its resumable conversation. Run copies are covered
1408
+ // by the company root already granted in these settings; the process env
1409
+ // still receives this run's actual AGENT_HOME.
1410
+ agentHome: asString(workspaceContext.agentHomeForPermissions, agentHome),
1383
1411
  companyId: agent.companyId,
1384
1412
  });
1385
1413
  skillCommandNotes.push(`Wrote Paperclip-managed Claude settings to ${paperclipClaudeSettings.filePath} (defaultMode=${paperclipClaudeSettings.defaultMode}${paperclipClaudeSettings.overrodeDontAsk ? "; overrode user dontAsk" : ""}, +${paperclipClaudeSettings.additionalDirectories.length} read root(s), +${paperclipClaudeSettings.allow.length} allow rule(s)).`);
@@ -1510,7 +1538,9 @@ async function buildRuntime(input) {
1510
1538
  defaultMode: paperclipClaudeSettings.defaultMode,
1511
1539
  }
1512
1540
  : null,
1513
- mcpServers: mcpIdentity,
1541
+ // Qualified built-in ACP harnesses reconnect to the supplied MCP servers
1542
+ // on session/load. Keep their conversation identity independent of tools.
1543
+ mcpServers: ["claude", "codex", "grok", "gemini", "kimi"].includes(acpxAgent) ? [] : mcpIdentity,
1514
1544
  ...(toolProfile === "read_only" ? { toolProfile } : {}),
1515
1545
  secretManifestHash: shortHash(secretManifest),
1516
1546
  // Fold the resolved adapter env (all applied user-configured values —
@@ -2204,16 +2234,13 @@ async function buildPrompt(ctx, resumedSession, env, paperclipAccess, paperclipT
2204
2234
  const renderedBootstrapPrompt = !resumedSession && bootstrapPromptTemplate.trim().length > 0
2205
2235
  ? renderTemplate(bootstrapPromptTemplate, templateData).trim()
2206
2236
  : "";
2207
- const taskContextNote = selectPaperclipTaskMarkdown(context, { resumedSession });
2208
- const externalChatTurn = isPaperclipExternalChatTurn(context.paperclipWake);
2209
- const wakePrompt = renderPaperclipWakePrompt(context.paperclipWake, {
2237
+ await hydrateFreshSessionHandoff(ctx, { resumedSession });
2238
+ const { taskContextNote, wakePrompt } = selectPaperclipPromptSections(context, {
2210
2239
  resumedSession,
2211
- conversationMode: context.conversationMode === true,
2212
- // The task-context markdown is the authoritative brief on this lane; keep
2213
- // the wake prompt's description copy out so the prompt carries it once.
2214
- suppressIssueDescription: taskContextNote.length > 0,
2240
+ includeCommunicationGuidance: false,
2215
2241
  paperclipAccess,
2216
2242
  });
2243
+ const externalChatTurn = isPaperclipExternalChatTurn(context.paperclipWake);
2217
2244
  const shouldUseResumeDeltaPrompt = resumedSession && wakePrompt.length > 0;
2218
2245
  const promptInstructionsPrefix = shouldUseResumeDeltaPrompt ? "" : instructionsPrefix;
2219
2246
  const renderedPrompt = shouldUseResumeDeltaPrompt || (externalChatTurn && !hasCustomPromptTemplate)
@@ -2225,6 +2252,7 @@ async function buildPrompt(ctx, resumedSession, env, paperclipAccess, paperclipT
2225
2252
  const prompt = joinPromptSections([
2226
2253
  promptInstructionsPrefix,
2227
2254
  renderedBootstrapPrompt,
2255
+ selectInitialCommunicationGuidance(context, { resumedSession }),
2228
2256
  wakePrompt,
2229
2257
  sessionHandoffNote,
2230
2258
  taskContextNote,
@@ -2717,12 +2745,13 @@ async function closeWarmHandle(input) {
2717
2745
  input.handles.delete(input.key);
2718
2746
  }
2719
2747
  clearWarmHandleTimer(input.entry);
2720
- await input.entry.runtime.close({
2748
+ const stopped = await input.entry.runtime.close({
2721
2749
  handle: input.entry.handle,
2722
2750
  reason: input.reason,
2723
2751
  discardPersistentState: input.discardPersistentState ?? false,
2724
- }).catch(() => { });
2752
+ }).then(() => true, () => false);
2725
2753
  flushChildStderr(input.entry.childStderrState);
2754
+ return stopped;
2726
2755
  }
2727
2756
  function warmHandleMatches(entry, runtime, handle) {
2728
2757
  return entry !== undefined && entry.runtime === runtime && entry.handle === handle;
@@ -3130,9 +3159,9 @@ export function createAcpxEngineExecutor(deps = {}) {
3130
3159
  : teardownErr instanceof Error
3131
3160
  ? teardownErr.message
3132
3161
  : String(teardownErr);
3133
- await ctx
3162
+ await withAdapterExecutionPhase(ctx, "phase_reporting", () => ctx
3134
3163
  .onLog("stderr", `[paperclip] ACPX teardown step "${step}" failed: ${reason}\n`)
3135
- .catch(() => { });
3164
+ .catch(() => { }));
3136
3165
  };
3137
3166
  // Emit one per-phase timing run-log event. It is not an OpenTelemetry
3138
3167
  // export and it is not a Telemetry event: it carries the phase name
@@ -3145,19 +3174,20 @@ export function createAcpxEngineExecutor(deps = {}) {
3145
3174
  const timedPhase = async (phase, run) => {
3146
3175
  const start = now();
3147
3176
  try {
3148
- await run();
3177
+ await withAdapterExecutionPhase(ctx, phase, run);
3149
3178
  }
3150
3179
  catch (error) {
3151
- await emitPhase(phase, start, "failed");
3180
+ await withAdapterExecutionPhase(ctx, "phase_reporting", () => emitPhase(phase, start, "failed"));
3152
3181
  throw error;
3153
3182
  }
3154
- await emitPhase(phase, start, "ok");
3183
+ await withAdapterExecutionPhase(ctx, "phase_reporting", () => emitPhase(phase, start, "ok"));
3155
3184
  };
3156
3185
  // The turn the run started. It is hoisted to the run scope so the settlement
3157
3186
  // `endSession` step can cancel a running turn before it closes the runtime
3158
3187
  // (the cancel-before-close order). The turn wrapper assigns it in `turnStart`.
3159
3188
  let activeTurn = null;
3160
3189
  let terminalFailureClassification = null;
3190
+ let terminalSessionFailure = null;
3161
3191
  // How the settlement `endSession` step must release the runtime for the path
3162
3192
  // this run took. Each exit path that acquired the runtime records it before it
3163
3193
  // returns; a build or create-runtime failure never registers the runtime, so
@@ -3268,7 +3298,14 @@ export function createAcpxEngineExecutor(deps = {}) {
3268
3298
  // same session still sees it. The borrow clears the entry's idle timer, so
3269
3299
  // the reused runtime cannot expire under its own timer while this run uses
3270
3300
  // it (today's `clearWarmHandleTimer(cached)` after the reuse decision).
3271
- const cached = canResume ? hostStore.borrow(prepared.sessionKey) : undefined;
3301
+ let cached = canResume ? hostStore.borrow(prepared.sessionKey) : undefined;
3302
+ if (cached && (ctx.context.refreshTools === true
3303
+ || asString(previousParams.mcpFingerprint, "") !== shortHash(prepared.mcpIdentity)
3304
+ // MCP bearer credentials are run-scoped, even for an unchanged catalog.
3305
+ || prepared.mcpServers.length > 0)) {
3306
+ await hostStore.discard(prepared.sessionKey);
3307
+ cached = undefined;
3308
+ }
3272
3309
  childStderrState = cached?.childStderrState ?? { logPath: null, pendingLiveLine: "" };
3273
3310
  processIdentitySink = cached?.processIdentitySink ?? {
3274
3311
  current: ctx.onSpawn,
@@ -3281,27 +3318,9 @@ export function createAcpxEngineExecutor(deps = {}) {
3281
3318
  flushChildStderr(childStderrState);
3282
3319
  childStderrState.logPath = prepared.childStderrLogPath;
3283
3320
  const persistedRuntimeStore = createRuntimeStore({ stateDir: prepared.stateDir });
3284
- const runtimeStore = {
3285
- async load(id) {
3286
- const record = await persistedRuntimeStore.load(id);
3287
- if (!record)
3288
- return undefined;
3289
- // ACPX resumes from the stored session options rather than the
3290
- // options passed to ensureSession. Keep conversation state, but
3291
- // launch the provider with this run's credentials and scratch paths.
3292
- return {
3293
- ...record,
3294
- acpx: {
3295
- ...record.acpx,
3296
- session_options: {
3297
- ...record.acpx?.session_options,
3298
- env: { ...prepared.env },
3299
- },
3300
- },
3301
- };
3302
- },
3303
- save: (record) => persistedRuntimeStore.save(record),
3304
- };
3321
+ // Resume with this run's launch environment; keep it out of the saved
3322
+ // conversation record without mutating the live runtime's options.
3323
+ const runtimeStore = createEphemeralSessionEnvironmentStore(persistedRuntimeStore, prepared.env);
3305
3324
  const runtimeOptions = {
3306
3325
  cwd: prepared.cwd,
3307
3326
  // Host-only spawn cwd for the relay proxy on the remote process-session
@@ -3741,6 +3760,8 @@ export function createAcpxEngineExecutor(deps = {}) {
3741
3760
  };
3742
3761
  let eventBreakdown = null;
3743
3762
  let eventCostUsd = null;
3763
+ let lastRuntimeEventAt = null;
3764
+ let observedRuntimeEvents = 0;
3744
3765
  // The turn-local state the sequence steps share. `promptBuild` sets the
3745
3766
  // prompt, `preTurnUsage` sets the pre-turn status, `turnStart` sets the
3746
3767
  // active turn, and `turnFinalize` reads all three. `activeTurn` is the run-
@@ -3831,14 +3852,13 @@ export function createAcpxEngineExecutor(deps = {}) {
3831
3852
  timeoutMs: startTimeoutMs,
3832
3853
  signal,
3833
3854
  // The callback belongs to this turn, including when a runtime is reused.
3834
- // Raw provider text must never enter the result or the run log.
3835
- ...(deps.classifyTerminalSessionFailure
3836
- ? {
3837
- onTerminalSessionFailure: (failure) => {
3838
- terminalFailureClassification = deps.classifyTerminalSessionFailure(failure, new Date(now()));
3839
- },
3840
- }
3841
- : {}),
3855
+ // Diagnostics are retained even when an adapter has no recovery
3856
+ // classifier. Redact before bounding so partial secrets cannot leak.
3857
+ onTerminalSessionFailure: (failure) => {
3858
+ terminalFailureClassification = classifyToolDefinitionFailure(failure)
3859
+ ?? deps.classifyTerminalSessionFailure?.(failure, new Date(now())) ?? null;
3860
+ terminalSessionFailure = sanitizeTerminalSessionFailure(failure, prepared.env, ctx.authToken, parseObject(ctx.config.env));
3861
+ },
3842
3862
  });
3843
3863
  activeTurn = turn;
3844
3864
  // A latched sandbox duplex-channel loss otherwise has no way to reach
@@ -3895,7 +3915,7 @@ export function createAcpxEngineExecutor(deps = {}) {
3895
3915
  armStopDeadline();
3896
3916
  return {
3897
3917
  cancel: async (reason) => {
3898
- await turn.cancel({ reason });
3918
+ await withAdapterExecutionPhase(ctx, "cancel_turn", () => turn.cancel({ reason }));
3899
3919
  },
3900
3920
  };
3901
3921
  };
@@ -3914,6 +3934,8 @@ export function createAcpxEngineExecutor(deps = {}) {
3914
3934
  const toolTitles = new Map();
3915
3935
  const drainEvents = (async () => {
3916
3936
  for await (const event of turn.events) {
3937
+ lastRuntimeEventAt = now();
3938
+ observedRuntimeEvents += 1;
3917
3939
  // ACPX currently flattens client-side filesystem/terminal receipts
3918
3940
  // into status text. They cannot establish complete action outcomes.
3919
3941
  if (event.type === "status" && /^(fs|terminal)\//.test(event.text))
@@ -3972,6 +3994,16 @@ export function createAcpxEngineExecutor(deps = {}) {
3972
3994
  return await Promise.race([turn.result, lossDeadline.then(() => LOSS_DEADLINE_TERMINAL)]);
3973
3995
  };
3974
3996
  const stepTurnFinalize = async (input) => {
3997
+ // Capture runtime activity before usage reads, error logs, or cleanup
3998
+ // make an otherwise silent turn look active at its execution deadline.
3999
+ const lastEventAgeMs = lastRuntimeEventAt === null ? undefined : now() - lastRuntimeEventAt;
4000
+ const activityDiagnostics = {
4001
+ acpObservedEventCount: observedRuntimeEvents,
4002
+ ...(lastEventAgeMs !== undefined && Number.isSafeInteger(lastEventAgeMs) && lastEventAgeMs >= 0
4003
+ ? { acpLastEventAgeMs: lastEventAgeMs } : {}),
4004
+ acpPendingToolCount: [...interruptionTools.values()].filter((tool) => tool.status !== "completed" && tool.status !== "failed" && tool.status !== "cancelled").length,
4005
+ acpToolInventoryComplete: !incompleteToolInventory,
4006
+ };
3975
4007
  if (input.kind === "terminal") {
3976
4008
  const terminal = input.terminal;
3977
4009
  const timedOut = input.timedOut;
@@ -4055,11 +4087,14 @@ export function createAcpxEngineExecutor(deps = {}) {
4055
4087
  cancelTurnReason: null,
4056
4088
  skipRemoteClose: channelLost,
4057
4089
  };
4090
+ const failureDiagnostic = !timedOut && !channelLost && terminal.status === "failed"
4091
+ ? terminalSessionFailure
4092
+ : null;
4058
4093
  const errorMessage = timedOut
4059
4094
  ? formatAdapterExecutionTimeoutErrorMessage(prepared.timeoutResolution)
4060
4095
  : channelLost
4061
4096
  ? channelLostMessage
4062
- : resultErrorMessage(terminal);
4097
+ : formatTerminalSessionFailure(resultErrorMessage(terminal), failureDiagnostic);
4063
4098
  const terminalStopReason = terminal.status === "failed" ? terminal.error.message : terminal.stopReason;
4064
4099
  const classifiedFailure = !timedOut && !channelLost && terminal.status === "failed"
4065
4100
  ? terminalFailureClassification
@@ -4098,6 +4133,8 @@ export function createAcpxEngineExecutor(deps = {}) {
4098
4133
  costUsd: turnUsage.costUsd,
4099
4134
  resultJson: {
4100
4135
  status: channelLost ? "failed" : terminal.status,
4136
+ ...activityDiagnostics,
4137
+ ...(failureDiagnostic ? { terminalSessionFailure: failureDiagnostic } : {}),
4101
4138
  ...(classifiedFailure?.errorFamily ? { errorFamily: classifiedFailure.errorFamily } : {}),
4102
4139
  ...(classifiedFailure?.retryNotBefore
4103
4140
  ? {
@@ -4228,7 +4265,7 @@ export function createAcpxEngineExecutor(deps = {}) {
4228
4265
  ...referencedProjectStagingFailuresField,
4229
4266
  model: prepared.requestedModel || null,
4230
4267
  clearSession: clearSession || timedOut,
4231
- resultJson: { phase },
4268
+ resultJson: { phase, ...activityDiagnostics },
4232
4269
  summary: message,
4233
4270
  };
4234
4271
  // Return a typed failed completion so the coordinator settles for a cause
@@ -4334,7 +4371,7 @@ export function createAcpxEngineExecutor(deps = {}) {
4334
4371
  : baseSettlement;
4335
4372
  // Cancel a running turn before the close (the turn-error path).
4336
4373
  if (settlement.cancelTurnReason && activeTurn) {
4337
- await activeTurn.cancel({ reason: settlement.cancelTurnReason }).catch(() => { });
4374
+ await withAdapterExecutionPhase(ctx, "cancel_turn", () => activeTurn.cancel({ reason: settlement.cancelTurnReason }).catch(() => { }));
4338
4375
  }
4339
4376
  const existing = warmHandles.get(prepared.sessionKey);
4340
4377
  // Re-read the duplex control-channel disposition here, at the
@@ -4371,26 +4408,26 @@ export function createAcpxEngineExecutor(deps = {}) {
4371
4408
  existing) {
4372
4409
  // A matching warm entry closes through the warm store, which also
4373
4410
  // clears its idle timer and flushes its child stderr.
4374
- await closeWarmHandle({
4411
+ runtimeStopConfirmed = await withAdapterExecutionPhase(ctx, "close_session", () => closeWarmHandle({
4375
4412
  handles: warmHandles,
4376
4413
  key: prepared.sessionKey,
4377
4414
  entry: existing,
4378
4415
  reason: settlement.reason,
4379
4416
  discardPersistentState: settlement.discardPersistentState,
4380
- });
4417
+ }));
4381
4418
  return;
4382
4419
  }
4383
4420
  const onCloseError = settlement.recordCloseError || ctx.signal?.aborted
4384
4421
  ? (closeErr) => recordTeardownError("runtime-close", closeErr)
4385
4422
  : () => { };
4386
- await runtime
4423
+ await withAdapterExecutionPhase(ctx, "close_session", () => runtime
4387
4424
  .close({
4388
4425
  handle: settlement.handle,
4389
4426
  reason: settlement.reason,
4390
4427
  discardPersistentState: settlement.discardPersistentState,
4391
4428
  })
4392
4429
  .then(() => { runtimeStopConfirmed = true; })
4393
- .catch(onCloseError);
4430
+ .catch(onCloseError));
4394
4431
  if (settlement.dropWarmEntry && warmHandleMatches(existing, runtime, settlement.handle) && existing) {
4395
4432
  clearWarmHandleTimer(existing);
4396
4433
  warmHandles.delete(prepared.sessionKey);
@@ -4418,12 +4455,29 @@ export function createAcpxEngineExecutor(deps = {}) {
4418
4455
  // runs, so the host mints a `traceparent` for the provider spans, and the
4419
4456
  // per-task restore spans parent to `sandbox.syncBack`.
4420
4457
  syncBack: () => timedPhase("sync_back", async () => {
4421
- await runRuntimeSpan("sandbox.syncBack", async () => {
4422
- const restoreOutcome = await syncBackManagedHome(prepared);
4423
- if (!restoreOutcome.ok) {
4424
- workspaceRestoreFailureField = { workspaceRestoreFailure: restoreOutcome.code };
4425
- }
4426
- });
4458
+ // A transport error or terminal narrative cannot authorize collecting
4459
+ // mutable provider files. Require the owned close or local OS handle.
4460
+ const stopped = runtimeStopConfirmed || (!prepared.processSessionBridge && capturedProcessExited(processIdentitySink?.localProcess));
4461
+ try {
4462
+ if (stopped)
4463
+ await withAdapterExecutionPhase(ctx, "instruction_collection", () => ctx.onProviderStopped?.());
4464
+ }
4465
+ catch {
4466
+ // Match ACP's fail-soft teardown policy without describing this as
4467
+ // a workspace restore failure or exposing paths from a raw error.
4468
+ await recordTeardownError("instruction-collection", new Error("Instruction collection failed after provider stop. No instruction save is claimed."));
4469
+ }
4470
+ finally {
4471
+ await withAdapterExecutionPhase(ctx, "workspace_restore", () => runRuntimeSpan("sandbox.syncBack", async () => {
4472
+ const restoreOutcome = await syncBackManagedHome(prepared);
4473
+ if (!restoreOutcome.ok) {
4474
+ workspaceRestoreFailureField = {
4475
+ workspaceRestoreFailure: restoreOutcome.code,
4476
+ ...(restoreOutcome.diagnostic ? { workspaceRestoreDiagnostic: restoreOutcome.diagnostic } : {}),
4477
+ };
4478
+ }
4479
+ }));
4480
+ }
4427
4481
  }),
4428
4482
  // The staging lease releases as the run's final act, AFTER the coordinator
4429
4483
  // reproduces the result, in the run root `finally` below. This step stays a
@@ -4492,6 +4546,15 @@ export function createAcpxEngineExecutor(deps = {}) {
4492
4546
  } },
4493
4547
  };
4494
4548
  }
4549
+ // Preserve the policy resolved for this execution target. The server
4550
+ // cannot reconstruct sandbox defaults from the stored agent config.
4551
+ capturedResult = {
4552
+ ...capturedResult,
4553
+ resultJson: {
4554
+ ...capturedResult.resultJson,
4555
+ adapterExecutionTimeout: { ...prepared.timeoutResolution },
4556
+ },
4557
+ };
4495
4558
  // The sync-back settlement step runs before this reproduces the result
4496
4559
  // (settlement precedes reproduction), so a failed workspace restore is
4497
4560
  // already recorded by the time we get here. Merge it into `resultJson`