@stigmer/runner 3.11.0 → 3.12.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 (239) hide show
  1. package/README.md +2 -0
  2. package/dist/.build-fingerprint +1 -1
  3. package/dist/activities/discover-mcp-server.d.ts +7 -0
  4. package/dist/activities/discover-mcp-server.js +6 -4
  5. package/dist/activities/discover-mcp-server.js.map +1 -1
  6. package/dist/activities/emit-event.d.ts +14 -2
  7. package/dist/activities/emit-event.js +52 -17
  8. package/dist/activities/emit-event.js.map +1 -1
  9. package/dist/activities/execute-cursor/attachment-resolver.d.ts +11 -0
  10. package/dist/activities/execute-cursor/attachment-resolver.js +8 -0
  11. package/dist/activities/execute-cursor/attachment-resolver.js.map +1 -1
  12. package/dist/activities/execute-cursor/blueprint-resolver.d.ts +1 -9
  13. package/dist/activities/execute-cursor/blueprint-resolver.js +0 -1
  14. package/dist/activities/execute-cursor/blueprint-resolver.js.map +1 -1
  15. package/dist/activities/execute-cursor/error-classifier.d.ts +19 -0
  16. package/dist/activities/execute-cursor/error-classifier.js +34 -3
  17. package/dist/activities/execute-cursor/error-classifier.js.map +1 -1
  18. package/dist/activities/execute-cursor/index.d.ts +52 -12
  19. package/dist/activities/execute-cursor/index.js +141 -67
  20. package/dist/activities/execute-cursor/index.js.map +1 -1
  21. package/dist/activities/execute-cursor/model-pricing-data.d.ts +2 -0
  22. package/dist/activities/execute-cursor/model-pricing-data.js +13 -3
  23. package/dist/activities/execute-cursor/model-pricing-data.js.map +1 -1
  24. package/dist/activities/execute-cursor/prompt-builder.d.ts +48 -11
  25. package/dist/activities/execute-cursor/prompt-builder.js +57 -10
  26. package/dist/activities/execute-cursor/prompt-builder.js.map +1 -1
  27. package/dist/activities/execute-cursor/turn-recovery.d.ts +52 -0
  28. package/dist/activities/execute-cursor/turn-recovery.js +193 -0
  29. package/dist/activities/execute-cursor/turn-recovery.js.map +1 -0
  30. package/dist/activities/execute-deep-agent/attachment-injector.d.ts +11 -0
  31. package/dist/activities/execute-deep-agent/attachment-injector.js +7 -1
  32. package/dist/activities/execute-deep-agent/attachment-injector.js.map +1 -1
  33. package/dist/activities/execute-deep-agent/index.js +0 -15
  34. package/dist/activities/execute-deep-agent/index.js.map +1 -1
  35. package/dist/activities/execute-deep-agent/mcp-gate.d.ts +0 -2
  36. package/dist/activities/execute-deep-agent/mcp-gate.js +0 -1
  37. package/dist/activities/execute-deep-agent/mcp-gate.js.map +1 -1
  38. package/dist/activities/execute-deep-agent/prompt-builder.d.ts +7 -8
  39. package/dist/activities/execute-deep-agent/prompt-builder.js +14 -7
  40. package/dist/activities/execute-deep-agent/prompt-builder.js.map +1 -1
  41. package/dist/activities/execute-deep-agent/setup.d.ts +0 -10
  42. package/dist/activities/execute-deep-agent/setup.js +22 -49
  43. package/dist/activities/execute-deep-agent/setup.js.map +1 -1
  44. package/dist/activities/execute-deep-agent/shell-env.d.ts +5 -1
  45. package/dist/activities/execute-deep-agent/shell-env.js +7 -9
  46. package/dist/activities/execute-deep-agent/shell-env.js.map +1 -1
  47. package/dist/activities/execute-deep-agent/subagent-transformer.js +7 -1
  48. package/dist/activities/execute-deep-agent/subagent-transformer.js.map +1 -1
  49. package/dist/activities/execute-deep-agent/subagent-wiring.d.ts +17 -3
  50. package/dist/activities/execute-deep-agent/subagent-wiring.js +14 -2
  51. package/dist/activities/execute-deep-agent/subagent-wiring.js.map +1 -1
  52. package/dist/activities/hydrate-workflow-execution.js +8 -5
  53. package/dist/activities/hydrate-workflow-execution.js.map +1 -1
  54. package/dist/activities/run-command.d.ts +5 -2
  55. package/dist/activities/run-command.js +22 -10
  56. package/dist/activities/run-command.js.map +1 -1
  57. package/dist/activities/run-env.d.ts +40 -0
  58. package/dist/activities/run-env.js +66 -0
  59. package/dist/activities/run-env.js.map +1 -0
  60. package/dist/bootstrap.js +5 -0
  61. package/dist/bootstrap.js.map +1 -1
  62. package/dist/client/stigmer-client.d.ts +83 -26
  63. package/dist/client/stigmer-client.js +88 -24
  64. package/dist/client/stigmer-client.js.map +1 -1
  65. package/dist/config.d.ts +6 -5
  66. package/dist/config.js.map +1 -1
  67. package/dist/encryption/config.d.ts +41 -9
  68. package/dist/encryption/config.js +63 -23
  69. package/dist/encryption/config.js.map +1 -1
  70. package/dist/middleware/index.d.ts +3 -0
  71. package/dist/middleware/index.js +7 -0
  72. package/dist/middleware/index.js.map +1 -1
  73. package/dist/middleware/path-normalization.d.ts +57 -0
  74. package/dist/middleware/path-normalization.js +109 -0
  75. package/dist/middleware/path-normalization.js.map +1 -0
  76. package/dist/middleware/types.d.ts +8 -0
  77. package/dist/payload-codecs.d.ts +2 -1
  78. package/dist/payload-codecs.js +7 -3
  79. package/dist/payload-codecs.js.map +1 -1
  80. package/dist/runner-manager.js +17 -3
  81. package/dist/runner-manager.js.map +1 -1
  82. package/dist/runner.js +7 -3
  83. package/dist/runner.js.map +1 -1
  84. package/dist/shared/artifact-storage.d.ts +34 -1
  85. package/dist/shared/artifact-storage.js +53 -10
  86. package/dist/shared/artifact-storage.js.map +1 -1
  87. package/dist/shared/attachment-download-urls.d.ts +62 -0
  88. package/dist/shared/attachment-download-urls.js +79 -0
  89. package/dist/shared/attachment-download-urls.js.map +1 -0
  90. package/dist/shared/channel-attachment.d.ts +1 -1
  91. package/dist/shared/channel-attachment.js +1 -1
  92. package/dist/shared/checkpointer/http-saver.d.ts +4 -20
  93. package/dist/shared/checkpointer/http-saver.js +15 -57
  94. package/dist/shared/checkpointer/http-saver.js.map +1 -1
  95. package/dist/shared/conversation-attachment.d.ts +2 -3
  96. package/dist/shared/conversation-attachment.js +2 -3
  97. package/dist/shared/conversation-attachment.js.map +1 -1
  98. package/dist/shared/conversation-catchup.d.ts +9 -2
  99. package/dist/shared/conversation-catchup.js +39 -6
  100. package/dist/shared/conversation-catchup.js.map +1 -1
  101. package/dist/shared/grpc-retry.d.ts +5 -1
  102. package/dist/shared/grpc-retry.js +5 -1
  103. package/dist/shared/grpc-retry.js.map +1 -1
  104. package/dist/shared/http-retry.d.ts +51 -9
  105. package/dist/shared/http-retry.js +60 -9
  106. package/dist/shared/http-retry.js.map +1 -1
  107. package/dist/shared/mcp-manager.d.ts +8 -4
  108. package/dist/shared/mcp-manager.js +30 -4
  109. package/dist/shared/mcp-manager.js.map +1 -1
  110. package/dist/shared/mcp-schema-sanitizer.d.ts +70 -0
  111. package/dist/shared/mcp-schema-sanitizer.js +197 -0
  112. package/dist/shared/mcp-schema-sanitizer.js.map +1 -0
  113. package/dist/shared/mcp-transport-guard.d.ts +2 -2
  114. package/dist/shared/mcp-transport-guard.js +2 -2
  115. package/dist/shared/model-client.d.ts +6 -0
  116. package/dist/shared/model-client.js +14 -3
  117. package/dist/shared/model-client.js.map +1 -1
  118. package/dist/shared/model-pricing-data.d.ts +2 -0
  119. package/dist/shared/model-pricing-data.js +13 -3
  120. package/dist/shared/model-pricing-data.js.map +1 -1
  121. package/dist/shared/model-registry.js +3 -2
  122. package/dist/shared/model-registry.js.map +1 -1
  123. package/dist/shared/plan-mode-permissions.d.ts +7 -0
  124. package/dist/shared/plan-mode-permissions.js +7 -0
  125. package/dist/shared/plan-mode-permissions.js.map +1 -1
  126. package/dist/shared/registry-endpoint.d.ts +20 -0
  127. package/dist/shared/registry-endpoint.js +25 -0
  128. package/dist/shared/registry-endpoint.js.map +1 -1
  129. package/dist/shared/runner-credential-keys.d.ts +24 -0
  130. package/dist/shared/runner-credential-keys.js +47 -0
  131. package/dist/shared/runner-credential-keys.js.map +1 -0
  132. package/dist/shared/synthesized-attachment.d.ts +4 -4
  133. package/dist/shared/synthesized-attachment.js +4 -4
  134. package/dist/workflow-engine/resolve.d.ts +19 -1
  135. package/dist/workflow-engine/resolve.js +37 -2
  136. package/dist/workflow-engine/resolve.js.map +1 -1
  137. package/dist/workflows/connect-mcp-server.js +2 -0
  138. package/dist/workflows/connect-mcp-server.js.map +1 -1
  139. package/dist/workflows/types.d.ts +8 -0
  140. package/package.json +3 -4
  141. package/src/__test-utils__/__tests__/vitest-global-setup.test.ts +37 -0
  142. package/src/__test-utils__/fake-artifact-storage.ts +10 -1
  143. package/src/__test-utils__/vitest-global-setup.ts +50 -0
  144. package/src/__tests__/bootstrap.test.ts +43 -0
  145. package/src/__tests__/encryption-codec.test.ts +53 -0
  146. package/src/__tests__/preflight.test.ts +15 -0
  147. package/src/activities/__tests__/discover-mcp-server.test.ts +39 -1
  148. package/src/activities/__tests__/error-classifier.test.ts +94 -2
  149. package/src/activities/__tests__/run-command.test.ts +221 -0
  150. package/src/activities/discover-mcp-server.ts +16 -2
  151. package/src/activities/emit-event.ts +65 -21
  152. package/src/activities/execute-cursor/__tests__/attachment-resolver.test.ts +63 -3
  153. package/src/activities/execute-cursor/__tests__/build-prompt.test.ts +173 -31
  154. package/src/activities/execute-cursor/__tests__/model-pricing.test.ts +62 -1
  155. package/src/activities/execute-cursor/__tests__/turn-recovery.test.ts +160 -0
  156. package/src/activities/execute-cursor/attachment-resolver.ts +24 -0
  157. package/src/activities/execute-cursor/blueprint-resolver.ts +1 -10
  158. package/src/activities/execute-cursor/error-classifier.ts +40 -3
  159. package/src/activities/execute-cursor/index.ts +175 -74
  160. package/src/activities/execute-cursor/model-pricing-data.ts +22 -3
  161. package/src/activities/execute-cursor/prompt-builder.ts +89 -22
  162. package/src/activities/execute-cursor/turn-recovery.ts +208 -0
  163. package/src/activities/execute-deep-agent/__tests__/attachment-injector.test.ts +105 -0
  164. package/src/activities/execute-deep-agent/__tests__/hitl-reject.test.ts +0 -2
  165. package/src/activities/execute-deep-agent/__tests__/hitl-resume-approve-all.test.ts +0 -1
  166. package/src/activities/execute-deep-agent/__tests__/hitl-resume-history.test.ts +0 -1
  167. package/src/activities/execute-deep-agent/__tests__/mcp-gate.test.ts +2 -7
  168. package/src/activities/execute-deep-agent/__tests__/plan-mode-path-normalization.test.ts +174 -0
  169. package/src/activities/execute-deep-agent/__tests__/prompt-builder.test.ts +71 -0
  170. package/src/activities/execute-deep-agent/__tests__/sequential-gate-resume.test.ts +0 -1
  171. package/src/activities/execute-deep-agent/__tests__/shell-env.test.ts +24 -10
  172. package/src/activities/execute-deep-agent/__tests__/subagent-plan-mode-permissions.test.ts +22 -17
  173. package/src/activities/execute-deep-agent/attachment-injector.ts +20 -1
  174. package/src/activities/execute-deep-agent/index.ts +0 -19
  175. package/src/activities/execute-deep-agent/mcp-gate.ts +0 -3
  176. package/src/activities/execute-deep-agent/prompt-builder.ts +27 -15
  177. package/src/activities/execute-deep-agent/setup.ts +22 -73
  178. package/src/activities/execute-deep-agent/shell-env.ts +8 -9
  179. package/src/activities/execute-deep-agent/subagent-transformer.ts +7 -1
  180. package/src/activities/execute-deep-agent/subagent-wiring.ts +26 -3
  181. package/src/activities/hydrate-workflow-execution.ts +8 -5
  182. package/src/activities/run-command.ts +25 -12
  183. package/src/activities/run-env.ts +79 -0
  184. package/src/bootstrap.ts +5 -0
  185. package/src/client/__tests__/stigmer-client.test.ts +104 -5
  186. package/src/client/stigmer-client.ts +128 -29
  187. package/src/config.ts +6 -5
  188. package/src/encryption/config.ts +87 -23
  189. package/src/middleware/__tests__/path-normalization.test.ts +140 -0
  190. package/src/middleware/index.ts +8 -0
  191. package/src/middleware/path-normalization.ts +125 -0
  192. package/src/middleware/types.ts +11 -0
  193. package/src/payload-codecs.ts +8 -2
  194. package/src/runner-manager.ts +21 -3
  195. package/src/runner.ts +7 -3
  196. package/src/shared/__tests__/artifact-storage.test.ts +154 -2
  197. package/src/shared/__tests__/attachment-download-urls.test.ts +70 -0
  198. package/src/shared/__tests__/channel-attachment.test.ts +2 -2
  199. package/src/shared/__tests__/conversation-catchup.test.ts +24 -0
  200. package/src/shared/__tests__/http-retry.test.ts +5 -4
  201. package/src/shared/__tests__/mcp-manager.test.ts +53 -13
  202. package/src/shared/__tests__/mcp-schema-sanitizer.test.ts +267 -0
  203. package/src/shared/__tests__/model-client.test.ts +46 -0
  204. package/src/shared/__tests__/model-pricing.test.ts +62 -2
  205. package/src/shared/__tests__/model-registry.test.ts +22 -0
  206. package/src/shared/__tests__/synthesized-attachment.test.ts +19 -41
  207. package/src/shared/artifact-storage.ts +86 -10
  208. package/src/shared/attachment-download-urls.ts +98 -0
  209. package/src/shared/channel-attachment.ts +1 -1
  210. package/src/shared/checkpointer/http-saver.ts +15 -60
  211. package/src/shared/conversation-attachment.ts +2 -3
  212. package/src/shared/conversation-catchup.ts +39 -6
  213. package/src/shared/grpc-retry.ts +5 -1
  214. package/src/shared/http-retry.ts +98 -9
  215. package/src/shared/mcp-manager.ts +35 -4
  216. package/src/shared/mcp-schema-sanitizer.ts +224 -0
  217. package/src/shared/mcp-transport-guard.ts +2 -2
  218. package/src/shared/model-client.ts +21 -3
  219. package/src/shared/model-pricing-data.ts +22 -3
  220. package/src/shared/model-registry.ts +7 -2
  221. package/src/shared/plan-mode-permissions.ts +7 -0
  222. package/src/shared/registry-endpoint.ts +27 -0
  223. package/src/shared/runner-credential-keys.ts +46 -0
  224. package/src/shared/synthesized-attachment.ts +4 -4
  225. package/src/workflow-engine/__tests__/tasks/emit-event.test.ts +127 -49
  226. package/src/workflow-engine/resolve.ts +48 -2
  227. package/src/workflows/__tests__/connect-mcp-server.test.ts +25 -0
  228. package/src/workflows/connect-mcp-server.ts +2 -0
  229. package/src/workflows/types.ts +8 -0
  230. package/dist/activities/execute-cursor/mcp-config.d.ts +0 -30
  231. package/dist/activities/execute-cursor/mcp-config.js +0 -39
  232. package/dist/activities/execute-cursor/mcp-config.js.map +0 -1
  233. package/dist/shared/datastore-attachment.d.ts +0 -99
  234. package/dist/shared/datastore-attachment.js +0 -184
  235. package/dist/shared/datastore-attachment.js.map +0 -1
  236. package/src/activities/execute-cursor/mcp-config.ts +0 -66
  237. package/src/activities/execute-deep-agent/__tests__/datastore-degradation.test.ts +0 -104
  238. package/src/shared/__tests__/datastore-attachment.test.ts +0 -293
  239. package/src/shared/datastore-attachment.ts +0 -209
@@ -41,7 +41,7 @@ import type { Config } from "../../config.js";
41
41
  import { StigmerClient } from "../../client/stigmer-client.js";
42
42
  import { describeExecutionError } from "../../shared/model-error.js";
43
43
  import { resolveAgentWithTransportRecovery } from "./session-lifecycle.js";
44
- import type { AgentResolution, CreateAgentOptions, CreateCloudAgentOptions } from "./session-lifecycle.js";
44
+ import type { AgentResolution, AgentResolutionReason, CreateAgentOptions, CreateCloudAgentOptions } from "./session-lifecycle.js";
45
45
  import { CursorMode } from "@stigmer/protos/ai/stigmer/agentic/session/v1/enum_pb";
46
46
  import { determineCursorMode, isCloudMode } from "./cursor-mode.js";
47
47
  import { MessageAccumulator, cancelInProgressSubAgentProtos, collapseRedundantToolCallTwins } from "./message-translator.js";
@@ -72,7 +72,6 @@ import { StreamingUpdateScheduler, loadStreamingConfig } from "../../shared/stre
72
72
  import { createCursorEventRecorder } from "./cursor-event-recorder.js";
73
73
  import { resolveMcpServers, toCursorMcpConfig, validateMcpServerEnv } from "./mcp-resolver.js";
74
74
  import { resolveMcpTransportPosture } from "../../shared/mcp-transport-guard.js";
75
- import { synthesizeDatastoreAttachment } from "../../shared/datastore-attachment.js";
76
75
  import {
77
76
  discoverChannelMessaging,
78
77
  synthesizeChannelAttachment,
@@ -92,7 +91,8 @@ import { buildCursorSubAgentDefinitions } from "./subagent-config.js";
92
91
  import { resolveSkills } from "./skill-resolver.js";
93
92
  import { removeStigmerSymlink } from "../../shared/workspace/stigmer-link.js";
94
93
  import { resolveAttachments } from "./attachment-resolver.js";
95
- import { buildEnhancedPrompt, buildReinvocationPrompt, formatConversationCatchupSection, formatInputFiles, formatInteractionModePrefix, formatImplementPlanSection } from "./prompt-builder.js";
94
+ import { buildEnhancedPrompt, buildHitlRecoveryPrompt, buildReinvocationPrompt, formatConversationCatchupSection, formatInputFiles, formatInteractionModePrefix, formatImplementPlanSection } from "./prompt-builder.js";
95
+ import { composeTurnRecoveryDigest } from "./turn-recovery.js";
96
96
  import { installHitlGate, removeHitlGate } from "./workspace-setup.js";
97
97
  import { ensureHitlDir } from "../../shared/workspace/platform-dir.js";
98
98
  import {
@@ -664,35 +664,16 @@ async function executeCursorInner(
664
664
  ?? config.stigmerTokenRef?.current
665
665
  ?? config.stigmerToken;
666
666
 
667
- // Phase 4a2: Synthesize the datastore records attachment (T05).
668
- // Deliberately AFTER resolve + backfill: the attachment has no
669
- // McpServerUsage and reports discovered capabilities, so the
670
- // backfill's destructiveHint tightener can never force-gate
671
- // delete_record (which on channels would be silently skipped).
672
- // Empty approval maps keep it approval-free by construction.
673
- if (blueprint.datastoreUsages.length > 0) {
674
- const attachment = synthesizeDatastoreAttachment(blueprint.datastoreUsages, {
675
- bridgeEndpoint: config.mcpBridgeEndpoint,
676
- credential: attachmentCredential,
677
- backendEndpoint: config.stigmerBackendEndpoint,
678
- });
679
- if (attachment) {
680
- const resolvedServers = injectSynthesizedAttachment(
681
- mcpResolution.resolvedServers, attachment, "datastore records",
682
- );
683
- mcpResolution = {
684
- resolvedServers,
685
- cursorConfig: toCursorMcpConfig(resolvedServers),
686
- };
687
- }
688
- }
689
-
690
- // Phase 4a3: Synthesize the channel messaging attachment (DD-006
691
- // D7/D8), the records attachment's twin. The discovery read is the
692
- // attachment decision — the control plane runs the SAME candidate
693
- // computation the send authorization uses — and every failure mode
694
- // (no channel, OSS, registry down, pre-3a control plane) degrades
695
- // to honest absence: no tool, no section, execution unharmed.
667
+ // Phase 4a2: Synthesize the channel messaging attachment (DD-006
668
+ // D7/D8). Deliberately AFTER resolve + backfill: the attachment has
669
+ // no McpServerUsage and reports discovered capabilities, so the
670
+ // backfill's destructiveHint tightener can never force-gate its
671
+ // tools; empty approval maps keep it approval-free by construction.
672
+ // The discovery read is the attachment decision — the control plane
673
+ // runs the SAME candidate computation the send authorization uses —
674
+ // and every failure mode (no channel, OSS, registry down, pre-3a
675
+ // control plane) degrades to honest absence: no tool, no section,
676
+ // execution unharmed.
696
677
  const channelMessaging = await discoverChannelMessaging(client, exchangedRunnerToken);
697
678
  if (channelMessaging.length > 0) {
698
679
  const attachment = synthesizeChannelAttachment(channelMessaging, {
@@ -801,6 +782,7 @@ async function executeCursorInner(
801
782
  const attachmentEntries = attachmentResults.map((a) => ({
802
783
  path: a.relativePath,
803
784
  ...(a.renamedFrom !== undefined ? { renamedFrom: a.renamedFrom } : {}),
785
+ ...(a.downloadUrl !== undefined ? { downloadUrl: a.downloadUrl } : {}),
804
786
  }));
805
787
  // Vision facts, derived once from the single resolution result: the
806
788
  // images the model will see inline (in attachment order) and the ones
@@ -1146,13 +1128,13 @@ async function executeCursorInner(
1146
1128
  instructions: blueprint.instructions,
1147
1129
  userMessage: spec.message,
1148
1130
  skills: skillMetadata,
1149
- datastoreUsages: blueprint.datastoreUsages,
1150
1131
  channelMessaging,
1151
1132
  subAgents: blueprint.subAgents,
1152
1133
  workspaceDirs: blueprint.workspaceDirs,
1153
1134
  workspaceFileRefs: spec.workspaceFileRefs ?? [],
1154
1135
  attachments: attachmentEntries,
1155
1136
  vision: visionPromptInfo,
1137
+ downloadUrlKind: artifactStorage?.downloadUrlKind,
1156
1138
  pendingApprovals: adjudicatedApprovals,
1157
1139
  appliedToolCallIds,
1158
1140
  interactionMode,
@@ -1161,28 +1143,43 @@ async function executeCursorInner(
1161
1143
  senderIdentity: readSenderIdentity(blueprint.sessionSpec.metadata),
1162
1144
  sessionContext: readSessionContext(blueprint.sessionSpec.metadata),
1163
1145
  conversationCatchup: readConversationCatchup(spec.conversationCatchup),
1146
+ // The turn's recorded transcript, seeded from the persisted execution
1147
+ // on a reinvocation (Phase 3). Consumed only by the HITL-recovery
1148
+ // shape — reached from HERE when the stored handle failed to resume
1149
+ // at resolution time (issue #366 crossing 2).
1150
+ turnRecoveryDigest: isReinvocation
1151
+ ? composeTurnRecoveryDigest(status.messages)
1152
+ : undefined,
1164
1153
  });
1165
1154
 
1166
- // Phase 10a: Inject structured output instruction for Cursor harness
1167
- let effectivePrompt = prompt;
1168
- if (structuredOutputSchema) {
1169
- const schemaStr = JSON.stringify(structuredOutputSchema, null, 2);
1170
- effectivePrompt += `\n\n---\nCRITICAL OUTPUT REQUIREMENT:\nYour final response MUST be a single valid JSON object (no markdown, no commentary, no code fences) that matches this schema:\n${schemaStr}\n\nRespond with ONLY the JSON object. Nothing else.`;
1171
- }
1155
+ // Phase 10a: Inject the structured output instruction for the Cursor
1156
+ // harness. A per-turn directive, so like buildFromPlan it must ride every
1157
+ // prompt this turn sends — the primary AND the poisoned-handle recovery
1158
+ // rebuild (the transport retry re-sends effectivePrompt and inherits it).
1159
+ const withStructuredOutputDirective = (basePrompt: string): string =>
1160
+ appendStructuredOutputDirective(basePrompt, structuredOutputSchema);
1161
+ const effectivePrompt = withStructuredOutputDirective(prompt);
1172
1162
 
1173
1163
  // Phase 10a1: The turn's vision payload. The invariant is "images
1174
- // accompany the user's turn message where the message goes, they go":
1175
- // every send that delivers this turn's message carries them (the primary
1176
- // send and both fresh-agent recovery retries, whose empty conversations
1177
- // genuinely need the re-send), while a HITL re-invocation whose prompt
1178
- // carries no user message and whose resumed agent already holds the
1179
- // images in its native conversation carries none. Computed ONCE here so
1180
- // all send sites agree by construction.
1181
- const turnImages = isHitlReinvocation(approvalDecisions)
1182
- ? []
1183
- : toCursorImages(visionImages);
1184
- const toSendMessage = (sendPrompt: string): string | SDKUserMessage =>
1185
- turnImages.length > 0 ? { text: sendPrompt, images: turnImages } : sendPrompt;
1164
+ // accompany the user's turn message, wherever the conversation does not
1165
+ // already hold them" (primarySendCarriesImages): the ONLY send that
1166
+ // skips them is a HITL re-invocation of a successfully RESUMED agent,
1167
+ // whose native conversation carries the images from the original send.
1168
+ // Every send that starts an empty conversation re-delivers them the
1169
+ // ordinary first/fresh-agent primary send, the HITL primary send after a
1170
+ // resolution-time resume failure, and both mid-send recovery retries
1171
+ // (which always run on a fresh agent, so their sites pass turnImages
1172
+ // unconditionally). Attachments re-resolve on every invocation
1173
+ // (Phase 5b), so the bytes are in hand even on a re-invocation.
1174
+ const turnImages = toCursorImages(visionImages);
1175
+ const primarySendImages = primarySendCarriesImages(approvalDecisions, resolution.reason)
1176
+ ? turnImages
1177
+ : [];
1178
+ const toSendMessage = (
1179
+ sendPrompt: string,
1180
+ images: { data: string; mimeType: string }[],
1181
+ ): string | SDKUserMessage =>
1182
+ images.length > 0 ? { text: sendPrompt, images } : sendPrompt;
1186
1183
 
1187
1184
  // Phase 10a2: Log Stigmer preamble size for context trimming diagnostics
1188
1185
  const promptChars = effectivePrompt.length;
@@ -1298,7 +1295,7 @@ async function executeCursorInner(
1298
1295
  // The stall watchdog is armed inside consumeCursorTurnStream (it needs the
1299
1296
  // run to cancel), stored on turnState.stallWatchdog so this shared onDelta can
1300
1297
  // reset it and the activity's finally can stop it as a backstop.
1301
- const run = await resolution.agent.send(toSendMessage(effectivePrompt), {
1298
+ const run = await resolution.agent.send(toSendMessage(effectivePrompt, primarySendImages), {
1302
1299
  onDelta: (event) => {
1303
1300
  if (!turnFirstEventEmitted) {
1304
1301
  turnFirstEventEmitted = true;
@@ -1669,10 +1666,11 @@ async function executeCursorInner(
1669
1666
  // poisoned-handle path leaked the fresh agent — it closed the stale one.)
1670
1667
  resolution = { ...resolution, agent: freshAgent, agentId: freshAgent.agentId, isNew: true };
1671
1668
  turnState.streamErrorMessage = undefined;
1672
- // The retry carries the turn's images too (same toSendMessage): the
1673
- // fresh agent's conversation is empty, so skipping them here would
1674
- // silently lose the user's photo on a recovered turn.
1675
- const retryRun = await freshAgent.send(toSendMessage(retryPrompt), {
1669
+ // The retry always carries the turn's full image payload never the
1670
+ // primary send's HITL-trimmed set: the fresh agent's conversation is
1671
+ // empty, so skipping them here would silently lose the user's photo on
1672
+ // a recovered turn (issue #366's vision corollary).
1673
+ const retryRun = await freshAgent.send(toSendMessage(retryPrompt, turnImages), {
1676
1674
  onDelta: makeCursorTurnOnDelta(onDeltaDeps),
1677
1675
  });
1678
1676
  await consumeCursorTurnStream(retryRun, streamDeps);
@@ -1804,14 +1802,18 @@ async function executeCursorInner(
1804
1802
  instructions: blueprint.instructions,
1805
1803
  userMessage: spec.message,
1806
1804
  skills: skillMetadata,
1807
- datastoreUsages: blueprint.datastoreUsages,
1808
1805
  channelMessaging,
1809
1806
  subAgents: blueprint.subAgents,
1810
1807
  workspaceDirs: blueprint.workspaceDirs,
1811
1808
  workspaceFileRefs: spec.workspaceFileRefs ?? [],
1812
1809
  attachments: attachmentEntries,
1813
1810
  vision: visionPromptInfo,
1811
+ downloadUrlKind: artifactStorage?.downloadUrlKind,
1814
1812
  pendingApprovals: adjudicatedApprovals,
1813
+ // Without the applied set, the HITL-recovery prompt would tell
1814
+ // the fresh agent to carry out writes the runner already
1815
+ // exact-applied (the primary call at Phase 10 passes it too).
1816
+ appliedToolCallIds,
1815
1817
  interactionMode,
1816
1818
  // buildFromPlan was silently dropped here until T03 Sitting 3 —
1817
1819
  // a build turn that hit handle recovery lost its directive. The
@@ -1822,6 +1824,10 @@ async function executeCursorInner(
1822
1824
  senderIdentity: readSenderIdentity(blueprint.sessionSpec.metadata),
1823
1825
  sessionContext: readSessionContext(blueprint.sessionSpec.metadata),
1824
1826
  conversationCatchup: readConversationCatchup(spec.conversationCatchup),
1827
+ // Composed fresh (not reused from Phase 10): the failed primary
1828
+ // stream may have appended partial work onto status.messages,
1829
+ // and the replacement agent should know about that too.
1830
+ turnRecoveryDigest: composeTurnRecoveryDigest(status.messages),
1825
1831
  });
1826
1832
 
1827
1833
  console.log(
@@ -1837,7 +1843,14 @@ async function executeCursorInner(
1837
1843
  console.warn("Failed to update session with fresh agentId (non-fatal):", updateErr);
1838
1844
  }
1839
1845
 
1840
- const outcome = await runRecoveryStream(freshAgent, freshPrompt);
1846
+ // Same per-turn directive rule as buildFromPlan above: a
1847
+ // structured-output turn keeps its output contract on the rebuilt
1848
+ // prompt (the transport retry re-sends effectivePrompt and
1849
+ // inherits it without help).
1850
+ const outcome = await runRecoveryStream(
1851
+ freshAgent,
1852
+ withStructuredOutputDirective(freshPrompt),
1853
+ );
1841
1854
  if (!outcome.proceeded) {
1842
1855
  if (outcome.terminal.kind === "return") return slimStatus(status);
1843
1856
  throw new CancelledFailure(outcome.terminal.message);
@@ -2335,8 +2348,6 @@ export interface BuildPromptInput {
2335
2348
  instructions: string;
2336
2349
  userMessage: string;
2337
2350
  skills: import("./prompt-builder.js").SkillMetadata[];
2338
- /** Datastores attached via `datastore_usages` — the `<available_datastores>` section. */
2339
- datastoreUsages?: import("@stigmer/protos/ai/stigmer/agentic/agent/v1/spec_pb").DatastoreUsage[];
2340
2351
  /**
2341
2352
  * Serving proactive channels + their templates (the DD-006 D2
2342
2353
  * discovery read) — the `<available_channel_templates>` section.
@@ -2356,6 +2367,12 @@ export interface BuildPromptInput {
2356
2367
  * catchup — it rides both the enhanced prompt and a resumed turn's prefix.
2357
2368
  */
2358
2369
  vision?: import("./prompt-builder.js").VisionPromptInfo;
2370
+ /**
2371
+ * What kind of URL the turn's storage backend mints (issue #532) — keys
2372
+ * the input-files hand-off wording. Sourced from the resolved
2373
+ * artifactStorage's self-description; absent when no storage resolved.
2374
+ */
2375
+ downloadUrlKind?: import("../../shared/attachment-download-urls.js").DownloadUrlKind;
2359
2376
  pendingApprovals: import("@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/approval_pb").PendingApproval[];
2360
2377
  /**
2361
2378
  * Approved whole-file writes the runner already applied itself (exact-apply).
@@ -2400,6 +2417,15 @@ export interface BuildPromptInput {
2400
2417
  * usually blank.
2401
2418
  */
2402
2419
  conversationCatchup?: string;
2420
+ /**
2421
+ * The turn's recorded transcript rendered as digest lines
2422
+ * (turn-recovery.ts), composed from `status.messages` at the call site.
2423
+ * Consumed ONLY by the HITL-recovery shape — a fresh agent that replaced
2424
+ * a lost one mid-HITL needs the story of the work it no longer remembers
2425
+ * (issue #366); every other shape either has native context or no prior
2426
+ * work to tell.
2427
+ */
2428
+ turnRecoveryDigest?: string;
2403
2429
  }
2404
2430
 
2405
2431
  /**
@@ -2411,20 +2437,28 @@ export interface BuildPromptInput {
2411
2437
  * volume, or cloud server-side state) — there is no separate continuation
2412
2438
  * store. The prompt therefore depends only on how the agent was resolved:
2413
2439
  *
2414
- * 1. HITL reinvocation -> buildReinvocationPrompt (approval decisions;
2415
- * the resumed agent's native context carries
2416
- * the prior conversation)
2417
- * 2. resumed_successfully -> raw userMessage (native context carries it)
2418
- * 3. first execution / fresh -> buildEnhancedPrompt (full instructions +
2440
+ * 1. HITL reinvocation, -> buildReinvocationPrompt (approval decisions
2441
+ * resumed agent only; the resumed agent's native context
2442
+ * carries the prior conversation)
2443
+ * 2. HITL reinvocation, -> buildHitlRecoveryPrompt (full context +
2444
+ * fresh agent after the turn's recorded transcript + decisions;
2445
+ * resume failure the replacement agent's conversation is
2446
+ * empty, and both fresh-agent crossings —
2447
+ * resolution-time resume failure and mid-send
2448
+ * poisoned-handle recovery — land here by
2449
+ * keying on the reason, issue #366)
2450
+ * 3. resumed_successfully -> raw userMessage (native context carries it)
2451
+ * 4. first execution / fresh -> buildEnhancedPrompt (full instructions +
2419
2452
  * agent after resume failure skills; no prior conversation to inherit)
2420
2453
  */
2421
2454
  /**
2422
2455
  * Whether this activity invocation is a HITL re-invocation — the turn resumes
2423
2456
  * an agent purely to convey approval decisions, carrying NO user message.
2424
- * The single discriminator for everything that must ride with the user's
2425
- * message and nothing else: the reinvocation prompt shape (below) and the
2426
- * vision payload (images accompany the message; a resumed agent already holds
2427
- * them in its native conversation).
2457
+ * Discriminates the two surfaces that depend on the agent already holding
2458
+ * this turn's content natively the prompt shape (below) and the primary
2459
+ * send's vision payload but never alone: both pair it with
2460
+ * `resolution.reason`, because a FRESH agent mid-HITL holds nothing and
2461
+ * needs the full re-delivery (issue #366).
2428
2462
  */
2429
2463
  export function isHitlReinvocation(
2430
2464
  approvalDecisions: Map<string, ApprovalAction> | undefined,
@@ -2432,6 +2466,39 @@ export function isHitlReinvocation(
2432
2466
  return approvalDecisions !== undefined && approvalDecisions.size > 0;
2433
2467
  }
2434
2468
 
2469
+ /**
2470
+ * Whether the PRIMARY send delivers the turn's vision payload. The invariant
2471
+ * is "images accompany the user's turn message, wherever the conversation
2472
+ * does not already hold them" — so the only send that skips them is a HITL
2473
+ * re-invocation of a successfully RESUMED agent, whose native conversation
2474
+ * carries the images from the original send. A fresh agent mid-HITL
2475
+ * (resolution-time resume failure — issue #366's vision corollary) holds
2476
+ * nothing and needs the re-delivery. The mid-send recovery retries always
2477
+ * run on a fresh agent, so their send sites carry the payload
2478
+ * unconditionally rather than consulting this.
2479
+ */
2480
+ export function primarySendCarriesImages(
2481
+ approvalDecisions: Map<string, ApprovalAction> | undefined,
2482
+ reason: AgentResolutionReason,
2483
+ ): boolean {
2484
+ return !(isHitlReinvocation(approvalDecisions) && reason === "resumed_successfully");
2485
+ }
2486
+
2487
+ /**
2488
+ * Append the structured-output contract to a prompt when the execution
2489
+ * requests one. A per-turn directive (the buildFromPlan rule): it must ride
2490
+ * every prompt this turn sends — the primary send AND the poisoned-handle
2491
+ * recovery rebuild, which previously lost it (issue #366 ride-along).
2492
+ */
2493
+ export function appendStructuredOutputDirective(
2494
+ basePrompt: string,
2495
+ schema: Record<string, unknown> | undefined,
2496
+ ): string {
2497
+ if (!schema) return basePrompt;
2498
+ const schemaStr = JSON.stringify(schema, null, 2);
2499
+ return basePrompt + `\n\n---\nCRITICAL OUTPUT REQUIREMENT:\nYour final response MUST be a single valid JSON object (no markdown, no commentary, no code fences) that matches this schema:\n${schemaStr}\n\nRespond with ONLY the JSON object. Nothing else.`;
2500
+ }
2501
+
2435
2502
  export function buildPrompt(input: BuildPromptInput): string {
2436
2503
  const {
2437
2504
  resolution,
@@ -2448,10 +2515,44 @@ export function buildPrompt(input: BuildPromptInput): string {
2448
2515
  conversationCatchup,
2449
2516
  } = input;
2450
2517
 
2451
- // HITL reinvocation: the agent is resumed, so its native context carries the
2452
- // prior conversation; the reinvocation prompt conveys the approval decisions
2453
- // (and which approved writes the runner already exact-applied).
2518
+ // HITL reinvocation: the decisions-only prompt is correct ONLY while the
2519
+ // agent's native context still carries the prior conversation which only
2520
+ // resumed_successfully guarantees. Any other reason means a fresh agent
2521
+ // mid-HITL (in practice created_after_resume_failure: the stored handle
2522
+ // failed to resume, or a poisoned handle was replaced mid-send), which
2523
+ // gets the full recovery shape instead — enhanced context + the turn's
2524
+ // recorded transcript + the same decisions — because the bare decisions on
2525
+ // an empty conversation strand the agent with instructions and no story,
2526
+ // and the session inherits that amnesia permanently (issue #366).
2454
2527
  if (isHitlReinvocation(approvalDecisions)) {
2528
+ if (resolution.reason !== "resumed_successfully") {
2529
+ return buildHitlRecoveryPrompt(
2530
+ {
2531
+ instructions,
2532
+ userMessage,
2533
+ skills,
2534
+ channelMessaging: input.channelMessaging ?? [],
2535
+ subAgents,
2536
+ workspaceDirs,
2537
+ workspaceFileRefs,
2538
+ attachments,
2539
+ vision: input.vision,
2540
+ downloadUrlKind: input.downloadUrlKind,
2541
+ interactionMode,
2542
+ buildFromPlan,
2543
+ contextBridge: input.contextBridge,
2544
+ senderIdentity: input.senderIdentity,
2545
+ sessionContext: input.sessionContext,
2546
+ conversationCatchup,
2547
+ },
2548
+ {
2549
+ turnDigest: input.turnRecoveryDigest,
2550
+ pendingApprovals: input.pendingApprovals,
2551
+ approvalDecisions,
2552
+ appliedToolCallIds: input.appliedToolCallIds,
2553
+ },
2554
+ );
2555
+ }
2455
2556
  return buildReinvocationPrompt(
2456
2557
  input.pendingApprovals,
2457
2558
  approvalDecisions,
@@ -2478,7 +2579,7 @@ export function buildPrompt(input: BuildPromptInput): string {
2478
2579
  formatInteractionModePrefix(interactionMode),
2479
2580
  formatImplementPlanSection(buildFromPlan, attachments),
2480
2581
  attachments.length > 0
2481
- ? formatInputFiles(attachments, input.vision)
2582
+ ? formatInputFiles(attachments, input.vision, input.downloadUrlKind)
2482
2583
  : undefined,
2483
2584
  conversationCatchup !== undefined
2484
2585
  ? formatConversationCatchupSection(conversationCatchup)
@@ -2496,13 +2597,13 @@ export function buildPrompt(input: BuildPromptInput): string {
2496
2597
  instructions,
2497
2598
  userMessage,
2498
2599
  skills,
2499
- datastoreUsages: input.datastoreUsages ?? [],
2500
2600
  channelMessaging: input.channelMessaging ?? [],
2501
2601
  subAgents,
2502
2602
  workspaceDirs,
2503
2603
  workspaceFileRefs,
2504
2604
  attachments,
2505
2605
  vision: input.vision,
2606
+ downloadUrlKind: input.downloadUrlKind,
2506
2607
  interactionMode,
2507
2608
  buildFromPlan,
2508
2609
  contextBridge: input.contextBridge,
@@ -8,7 +8,12 @@
8
8
  * endpoint is unreachable.
9
9
  */
10
10
 
11
- import { resolveModelRegistryUrl, buildRegistryHeaders } from "../../shared/registry-endpoint.js";
11
+ import {
12
+ resolveModelRegistryUrl,
13
+ buildRegistryHeaders,
14
+ REGISTRY_RETRY_POLICY,
15
+ } from "../../shared/registry-endpoint.js";
16
+ import { fetchWithRetry } from "../../shared/http-retry.js";
12
17
 
13
18
  /** Per-million rates for a speed/mode variant (e.g. "fast") of a base model. */
14
19
  export interface CursorVariantPricing {
@@ -53,6 +58,10 @@ interface RegistryEntry {
53
58
  }
54
59
 
55
60
  const CACHE_TTL_MS = 3_600_000; // 1 hour
61
+ // Failed fetches are cached much shorter than successes (the model-registry.ts
62
+ // policy): a transient failure must not pin DEFAULT_PRICING — wrong rates for
63
+ // cost tracking — for a full hour.
64
+ const FAILURE_CACHE_TTL_MS = 60_000;
56
65
 
57
66
  const DEFAULT_PRICING: CursorModelPricing = {
58
67
  model: "unknown",
@@ -104,7 +113,11 @@ function parseVariants(
104
113
  }
105
114
 
106
115
  async function fetchFromApi(): Promise<readonly CursorModelPricing[]> {
107
- const res = await fetch(resolveModelRegistryUrl(), { headers: buildRegistryHeaders() });
116
+ const res = await fetchWithRetry(
117
+ resolveModelRegistryUrl(),
118
+ { headers: buildRegistryHeaders() },
119
+ REGISTRY_RETRY_POLICY,
120
+ );
108
121
  if (!res.ok) throw new Error(`Model registry fetch failed: ${res.status}`);
109
122
  const data: unknown = await res.json();
110
123
  const table = parsePricingTable(data);
@@ -138,7 +151,7 @@ export async function getPricingTable(): Promise<readonly CursorModelPricing[]>
138
151
  `Failed to fetch model registry from API, using default pricing: ${err}`,
139
152
  );
140
153
  const fallback = [DEFAULT_PRICING];
141
- cache = { data: fallback, expiresAt: Date.now() + CACHE_TTL_MS };
154
+ cache = { data: fallback, expiresAt: Date.now() + FAILURE_CACHE_TTL_MS };
142
155
  return fallback;
143
156
  })
144
157
  .finally(() => {
@@ -148,4 +161,10 @@ export async function getPricingTable(): Promise<readonly CursorModelPricing[]>
148
161
  return inflightFetch;
149
162
  }
150
163
 
164
+ /** Exposed for testing — resets the in-memory cache. */
165
+ export function _resetPricingCache(): void {
166
+ cache = null;
167
+ inflightFetch = null;
168
+ }
169
+
151
170
  export { DEFAULT_PRICING };
@@ -17,12 +17,11 @@
17
17
  */
18
18
 
19
19
  import { resolve } from "node:path";
20
- import type { DatastoreUsage, SubAgent } from "@stigmer/protos/ai/stigmer/agentic/agent/v1/spec_pb";
20
+ import type { SubAgent } from "@stigmer/protos/ai/stigmer/agentic/agent/v1/spec_pb";
21
21
  import type { PendingApproval } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/approval_pb";
22
22
  import { ApprovalAction, InteractionMode } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/enum_pb";
23
23
  import { formatContextBridgeText } from "../../shared/context-bridge.js";
24
24
  import { formatConversationCatchupText } from "../../shared/conversation-catchup.js";
25
- import { formatDatastoresSection } from "../../shared/datastore-attachment.js";
26
25
  import {
27
26
  formatChannelTemplatesSection,
28
27
  type ChannelMessagingInfo,
@@ -36,6 +35,11 @@ import {
36
35
  visionDisclosureLines,
37
36
  type NotViewableEntry,
38
37
  } from "../../shared/attachment-vision.js";
38
+ import {
39
+ downloadUrlDisclosureLine,
40
+ type DownloadUrlKind,
41
+ } from "../../shared/attachment-download-urls.js";
42
+ import { formatTurnRecoveryText } from "./turn-recovery.js";
39
43
  import { PLAN_MODE_DIRECTIVE } from "../../shared/plan-mode-prompt.js";
40
44
  import {
41
45
  buildImplementPlanDirective,
@@ -81,21 +85,18 @@ export interface AttachmentPromptEntry {
81
85
  path: string;
82
86
  /** Original filename, present only when a duplicate name was renamed. */
83
87
  renamedFrom?: string;
88
+ /**
89
+ * Download URL for the attachment's stored object, when one was minted
90
+ * (shared/attachment-download-urls.ts) — rendered beside the path so the
91
+ * agent can hand the file to tools that cannot read this filesystem.
92
+ */
93
+ downloadUrl?: string;
84
94
  }
85
95
 
86
96
  export interface EnhancedPromptOptions {
87
97
  instructions: string;
88
98
  userMessage: string;
89
99
  skills: SkillMetadata[];
90
- /**
91
- * Datastores attached via `datastore_usages` — rendered as the
92
- * `<available_datastores>` section (DD-005 SD-5, skills precedent)
93
- * pointing the model at the synthesized record tools. Always the healthy
94
- * rendering here: the Cursor SDK connects MCP itself, so this harness can
95
- * never observe the live roster — the section's standing
96
- * failure-disclosure instruction is its whole coverage (issue #325).
97
- */
98
- datastoreUsages?: DatastoreUsage[];
99
100
  /**
100
101
  * Serving proactive channels + their approved templates — rendered as
101
102
  * the `<available_channel_templates>` section (proactive-messaging
@@ -108,6 +109,12 @@ export interface EnhancedPromptOptions {
108
109
  attachments: AttachmentPromptEntry[];
109
110
  /** Inline/degraded image facts for the input-files section (T04 vision). */
110
111
  vision?: VisionPromptInfo;
112
+ /**
113
+ * What kind of URL the turn's storage backend mints — keys the input-files
114
+ * section's hand-off wording (attachment-download-urls.ts). One turn-level
115
+ * fact: all attachments ride the one configured storage.
116
+ */
117
+ downloadUrlKind?: DownloadUrlKind;
111
118
  interactionMode?: InteractionMode;
112
119
  /**
113
120
  * The execution is a Build-from-plan turn (spec.execution_config
@@ -182,10 +189,6 @@ export function buildEnhancedPrompt(options: EnhancedPromptOptions): string {
182
189
  sections.push(formatSkillsSection(options.skills));
183
190
  }
184
191
 
185
- if (options.datastoreUsages !== undefined && options.datastoreUsages.length > 0) {
186
- sections.push(formatDatastoresSection(options.datastoreUsages));
187
- }
188
-
189
192
  if (options.channelMessaging !== undefined && options.channelMessaging.length > 0) {
190
193
  // "" when nothing is sendable — the tool alone still serves text
191
194
  // sends inside a 24-hour window (DD-006 D6).
@@ -211,7 +214,7 @@ export function buildEnhancedPrompt(options: EnhancedPromptOptions): string {
211
214
  }
212
215
 
213
216
  if (options.attachments.length > 0) {
214
- sections.push(formatInputFiles(options.attachments, options.vision));
217
+ sections.push(formatInputFiles(options.attachments, options.vision, options.downloadUrlKind));
215
218
  }
216
219
 
217
220
  if (options.workspaceFileRefs.length > 0) {
@@ -338,6 +341,56 @@ export function buildReinvocationPrompt(
338
341
  return parts.join("\n\n");
339
342
  }
340
343
 
344
+ /**
345
+ * Build the prompt for a fresh agent that replaced a lost one MID-HITL —
346
+ * the stored handle failed to resume when the approval landed, or the
347
+ * resumed handle proved poisoned mid-send (issue #366, both crossings).
348
+ *
349
+ * A resumed agent gets {@link buildReinvocationPrompt} alone because its
350
+ * native conversation carries everything else. A replacement agent's
351
+ * conversation is EMPTY, so the bare decisions prompt would strand it with
352
+ * instructions and no story — and, because recovery repoints the session at
353
+ * the new agent, every later turn would inherit that amnesia. This prompt
354
+ * rebuilds the whole story in chronological-narrative order:
355
+ *
356
+ * 1. the full enhanced prompt (blueprint, standing context, protocol, and
357
+ * the turn's ORIGINAL user message as `<user_request>`),
358
+ * 2. `<turn_recovery>` — the state-loss disclosure plus the recorded
359
+ * transcript of the turn so far (turn-recovery.ts; rendered even with no
360
+ * transcript, since without the disclosure the decisions below would read
361
+ * as reactions to proposals this agent never made),
362
+ * 3. the reinvocation decisions verbatim — already-applied / approved /
363
+ * skipped semantics byte-identical to the resumed path, ending on its
364
+ * "continue the task" directive.
365
+ *
366
+ * Appending past `<user_request>` is the chronology speaking (request →
367
+ * work done → decisions → continue), the same shape as the activity-level
368
+ * structured-output suffix.
369
+ */
370
+ export function buildHitlRecoveryPrompt(
371
+ options: EnhancedPromptOptions,
372
+ recovery: {
373
+ turnDigest: string | undefined;
374
+ pendingApprovals: PendingApproval[];
375
+ approvalDecisions: Map<string, ApprovalAction>;
376
+ appliedToolCallIds?: ReadonlySet<string>;
377
+ },
378
+ ): string {
379
+ return [
380
+ buildEnhancedPrompt(options),
381
+ formatTurnRecoverySection(recovery.turnDigest),
382
+ buildReinvocationPrompt(
383
+ recovery.pendingApprovals,
384
+ recovery.approvalDecisions,
385
+ recovery.appliedToolCallIds,
386
+ ),
387
+ ].join("\n\n---\n\n");
388
+ }
389
+
390
+ export function formatTurnRecoverySection(digest: string | undefined): string {
391
+ return `<turn_recovery>\n${formatTurnRecoveryText(digest)}\n</turn_recovery>`;
392
+ }
393
+
341
394
  /**
342
395
  * Render a pending approval as a short human-readable action description for the
343
396
  * reinvocation prompt. Prefers the already-resolved approval message; falls back
@@ -497,15 +550,28 @@ export function formatWorkspaceContext(dirs: string[]): string {
497
550
  export function formatInputFiles(
498
551
  attachments: readonly AttachmentPromptEntry[],
499
552
  vision?: VisionPromptInfo,
553
+ downloadUrlKind?: DownloadUrlKind,
500
554
  ): string {
501
555
  // A duplicate-renamed file (attachment-naming.ts) discloses its original
502
556
  // name so the agent can connect "the two report.pdfs" in the user's
503
- // message to distinct files on disk.
504
- const entries = attachments.map((a) =>
505
- a.renamedFrom !== undefined
506
- ? `- \`${a.path}\` (renamed from duplicate '${a.renamedFrom}')`
507
- : `- \`${a.path}\``,
508
- );
557
+ // message to distinct files on disk. A file with a minted download URL
558
+ // (attachment-download-urls.ts) lists it beside the path for the remote
559
+ // hand-off story.
560
+ const entries = attachments.map((a) => {
561
+ const rename =
562
+ a.renamedFrom !== undefined
563
+ ? ` (renamed from duplicate '${a.renamedFrom}')`
564
+ : "";
565
+ const url = a.downloadUrl !== undefined ? ` — download URL: ${a.downloadUrl}` : "";
566
+ return `- \`${a.path}\`${rename}${url}`;
567
+ });
568
+ // The URL hand-off line (shared wording, attachment-download-urls.ts)
569
+ // renders only when some listed file actually carries a URL — its wording
570
+ // keys on what kind of URL the storage backend mints.
571
+ const urlDisclosure =
572
+ downloadUrlKind !== undefined && attachments.some((a) => a.downloadUrl !== undefined)
573
+ ? [downloadUrlDisclosureLine(downloadUrlKind)]
574
+ : [];
509
575
  // The vision lines (shared wording, attachment-vision.ts) tell the model
510
576
  // which of these files it can already SEE inline versus which degraded to
511
577
  // path-only — without them an agent silently ignores a photo the user
@@ -517,6 +583,7 @@ export function formatInputFiles(
517
583
  "<input_files>",
518
584
  "The following files have been provided as inputs. Read them when relevant to the task:",
519
585
  ...entries,
586
+ ...urlDisclosure,
520
587
  ...disclosure,
521
588
  "</input_files>",
522
589
  ].join("\n");