@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
@@ -14,9 +14,9 @@
14
14
  * The Java proxy calls Document.parse(json) which handles $binary
15
15
  * natively, and doc.toJson() emits the same format on reads.
16
16
  *
17
- * Every request runs through {@link HttpCheckpointSaver.fetchWithRetry}:
18
- * bounded exponential backoff on transient failures (classified by
19
- * http-retry.ts) with a per-request abort timeout. This loop is the ONLY
17
+ * Every request runs through the shared bounded-backoff loop
18
+ * (http-retry.ts's fetchWithRetry, with this saver's policy: transient
19
+ * classification + a per-request abort timeout). That loop is the ONLY
20
20
  * retry layer between an agent execution and its checkpoints — the deep
21
21
  * agent activity is deliberately non-retryable at the Temporal level
22
22
  * (maximumAttempts: 1; replaying a whole agent run is not safe), so before
@@ -39,7 +39,7 @@ import {
39
39
  } from "@langchain/langgraph-checkpoint";
40
40
  import type { CheckpointMetadata, PendingWrite } from "@langchain/langgraph-checkpoint";
41
41
  import type { RetryOptions } from "../grpc-retry.js";
42
- import { isRetryableFetchError, isRetryableHttpStatus } from "../http-retry.js";
42
+ import { fetchWithRetry, type FetchRetryPolicy } from "../http-retry.js";
43
43
 
44
44
  function encodeB64(data: Uint8Array): string {
45
45
  if (typeof Buffer !== "undefined") {
@@ -117,18 +117,10 @@ const DEFAULT_RETRY = {
117
117
  requestTimeoutMs: 30_000,
118
118
  } as const;
119
119
 
120
- function defaultDelay(ms: number): Promise<void> {
121
- return new Promise((resolve) => setTimeout(resolve, ms));
122
- }
123
-
124
120
  export class HttpCheckpointSaver extends BaseCheckpointSaver {
125
121
  private readonly baseUrl: string;
126
122
  private readonly headers: Record<string, string>;
127
- private readonly baseDelayMs: number;
128
- private readonly backoffFactor: number;
129
- private readonly maxRetries: number;
130
- private readonly requestTimeoutMs: number;
131
- private readonly delay: (ms: number) => Promise<void>;
123
+ private readonly retryPolicy: FetchRetryPolicy;
132
124
 
133
125
  constructor(
134
126
  proxyEndpoint: string,
@@ -141,55 +133,18 @@ export class HttpCheckpointSaver extends BaseCheckpointSaver {
141
133
  Authorization: `Bearer ${authToken}`,
142
134
  "Content-Type": "application/json",
143
135
  };
144
- this.baseDelayMs = options.baseDelayMs ?? DEFAULT_RETRY.baseDelayMs;
145
- this.backoffFactor = options.backoffFactor ?? DEFAULT_RETRY.backoffFactor;
146
- this.maxRetries = options.maxRetries ?? DEFAULT_RETRY.maxRetries;
147
- this.requestTimeoutMs = options.requestTimeoutMs ?? DEFAULT_RETRY.requestTimeoutMs;
148
- this.delay = options.delayFn ?? defaultDelay;
149
- }
150
-
151
- /**
152
- * `fetch` with bounded exponential backoff on transient failures and a
153
- * per-request abort timeout.
154
- *
155
- * Composition contract: on a non-retryable status — or when the budget is
156
- * exhausted on a retryable one — the last `Response` is RETURNED, so every
157
- * call site keeps its own `resp.ok` / 404 handling unchanged; this wrapper
158
- * changes when a response arrives, never what call sites do with it. It
159
- * throws only what `fetch` itself throws: a network-level rejection that is
160
- * non-retryable or has exhausted the budget.
161
- */
162
- private async fetchWithRetry(url: string, init?: RequestInit): Promise<Response> {
163
- for (let attempt = 0; ; attempt++) {
164
- const canRetry = attempt < this.maxRetries;
165
- let resp: Response;
166
- try {
167
- resp = await fetch(url, {
168
- ...init,
169
- signal: AbortSignal.timeout(this.requestTimeoutMs),
170
- });
171
- } catch (err) {
172
- if (canRetry && isRetryableFetchError(err)) {
173
- await this.backOff(attempt, String(err));
174
- continue;
175
- }
176
- throw err;
177
- }
178
- if (canRetry && isRetryableHttpStatus(resp.status)) {
179
- await this.backOff(attempt, `HTTP ${resp.status} ${resp.statusText}`);
180
- continue;
181
- }
182
- return resp;
183
- }
136
+ this.retryPolicy = {
137
+ label: "HttpCheckpointSaver",
138
+ baseDelayMs: options.baseDelayMs ?? DEFAULT_RETRY.baseDelayMs,
139
+ backoffFactor: options.backoffFactor ?? DEFAULT_RETRY.backoffFactor,
140
+ maxRetries: options.maxRetries ?? DEFAULT_RETRY.maxRetries,
141
+ requestTimeoutMs: options.requestTimeoutMs ?? DEFAULT_RETRY.requestTimeoutMs,
142
+ delayFn: options.delayFn,
143
+ };
184
144
  }
185
145
 
186
- private async backOff(attempt: number, reason: string): Promise<void> {
187
- const delayMs = this.baseDelayMs * Math.pow(this.backoffFactor, attempt);
188
- console.warn(
189
- `[HttpCheckpointSaver] retryable failure ` +
190
- `(attempt ${attempt + 1}/${this.maxRetries + 1}, retry in ${delayMs}ms): ${reason}`,
191
- );
192
- await this.delay(delayMs);
146
+ private fetchWithRetry(url: string, init?: RequestInit): Promise<Response> {
147
+ return fetchWithRetry(url, init, this.retryPolicy);
193
148
  }
194
149
 
195
150
  private async serializeTyped(obj: unknown): Promise<[string, BinaryObj]> {
@@ -1,8 +1,7 @@
1
1
  /**
2
2
  * The runner-synthesized conversation participation attachment
3
- * (channel-conversations DD-008 D-c, A14) — the third synthesized
4
- * attachment, on the datastore module's shape (a cheap local predicate,
5
- * not the channel module's discovery machinery).
3
+ * (channel-conversations DD-008 D-c, A14) — a cheap local predicate,
4
+ * not the channel module's discovery machinery.
6
5
  *
7
6
  * When the session IS a live channel conversation, the runner
8
7
  * synthesizes ONE MCP attachment serving `escalate_to_human`, so the
@@ -1,8 +1,15 @@
1
1
  /**
2
2
  * Conversation catchup (cloud channel-conversations DD-006): what happened on
3
3
  * a live channel conversation that the agent has not seen — customer messages
4
- * handled by a human teammate, the teammate's replies, platform notices the
5
- * customer received, notes, and the agent's own earlier escalations.
4
+ * handled by a human teammate, the teammate's replies, platform notices,
5
+ * notes, and the agent's own earlier escalations. Since cloud#347 a line
6
+ * whose send is known-undelivered carries a `(not delivered)` speaker
7
+ * annotation and an in-flight one carries `(sending)` — the digest no longer
8
+ * implies every public-lane line reached the customer. Since cloud#352 the
9
+ * digest also closes the feedback loop on send OUTCOMES: the agent's own
10
+ * dead-lettered sends surface as `You (not delivered):` lines, and a send
11
+ * that failed only after an earlier digest already conveyed it surfaces as a
12
+ * `System:` delivery-failure notice at the moment the failure became known.
6
13
  *
7
14
  * The cloud composes the CONTENT (bare `Customer:` / `Teammate:` / `System:` /
8
15
  * `You escalated:` / `Note:` lines, oldest first) on the execution spec's
@@ -29,16 +36,42 @@ import type { ConversationCatchup } from "@stigmer/protos/ai/stigmer/agentic/age
29
36
  * drift between them. Deliberately takeover-neutral: a digest can exist with
30
37
  * no human handoff at all (a failed turn's re-composed window), so the
31
38
  * preamble asserts only what is always true (the A15/A20 honesty bar).
39
+ *
40
+ * The send-status annotations (cloud#347) get an explicit exception from the
41
+ * don't-re-answer contract: a `(not delivered)` teammate reply is words the
42
+ * customer never got, so treating it as settled history would silently
43
+ * abandon whatever it was meant to convey. The preamble defines the
44
+ * annotations — the DD-013 split puts model-facing meaning here, while the
45
+ * cloud composer owns which lines earn them.
46
+ *
47
+ * The send-outcome lines (cloud#352, cloud triage DD-009) extend the same
48
+ * exception to the agent's OWN sends: `You (not delivered):` lines and
49
+ * `System:` delivery-failure notices mean the customer never got those
50
+ * words. The behavioral contract is owner-ruled (DD-009 Q-4): treat the
51
+ * failure as unfinished conversation business — weigh what still needs
52
+ * saying and re-say it naturally in the next turn — but never resend the
53
+ * failed text verbatim: a multi-chunk send can partially land, and a
54
+ * word-for-word repeat risks the customer reading the same message twice.
32
55
  */
33
56
  const CONVERSATION_CATCHUP_PREAMBLE =
34
57
  "Below is activity from this conversation that you have not seen — " +
35
58
  "oldest first. It may include customer messages that were handled by a " +
36
- "human teammate, the teammate's own replies, notices the customer " +
37
- "received, internal notes, and escalations you raised earlier. Treat it " +
59
+ "human teammate, the teammate's own replies, notices sent to the " +
60
+ "customer, internal notes, and escalations you raised earlier. Treat it " +
38
61
  "as conversation history you already know: do not answer or re-answer " +
39
62
  "these messages, do not repeat or summarize them back, and do not " +
40
- "mention any handoff unless asked. Continue from the customer's newest " +
41
- "message.";
63
+ "mention any handoff unless asked. One exception: lines marked " +
64
+ "(not delivered) never reached the customer, and lines marked (sending) " +
65
+ "were still on their way when this summary was built — the customer may " +
66
+ "not have seen those words, so weigh that when deciding what still needs " +
67
+ "saying. That includes your own words: a line marked " +
68
+ "You (not delivered), or a System line reporting that a message was not " +
69
+ "delivered, means the customer never received it. Treat such a failure " +
70
+ "as unfinished business — if what it said still matters, work it " +
71
+ "naturally into your reply in your own words, but never resend the " +
72
+ "failed text word-for-word (part of it may have reached the customer, " +
73
+ "and an exact repeat reads as a duplicate). " +
74
+ "Continue from the customer's newest message.";
42
75
 
43
76
  /**
44
77
  * Read the catchup digest from an execution spec's `conversation_catchup`.
@@ -6,7 +6,11 @@
6
6
  * `updateStatus` should back off and retry (transient transport errors) or
7
7
  * fail fast (deterministic errors). This module is intentionally just the
8
8
  * classification policy + its options type, kept small and separately tested;
9
- * the persist loop that consumes it lives with the rest of the persist logic.
9
+ * the persist loop that consumes it lives with the rest of the persist logic
10
+ * in status.ts — a single consumer whose loop is interwoven with persist
11
+ * semantics. (The HTTP twin, http-retry.ts, diverged there deliberately: with
12
+ * three fetch clients needing a byte-identical loop it hosts the shared
13
+ * fetchWithRetry too — see its module doc.)
10
14
  */
11
15
 
12
16
  import { ConnectError, Code } from "@connectrpc/connect";
@@ -1,14 +1,17 @@
1
1
  /**
2
- * HTTP error classification for side-channel proxy retries.
2
+ * HTTP error classification AND the shared bounded-backoff loop for
3
+ * side-channel proxy retries.
3
4
  *
4
5
  * The runner reaches stigmer-service's Side-Channel Proxy (`/v1/proxy/...`)
5
- * over plain `fetch` from several clients the checkpoint saver today;
6
- * artifact storage, the LLM proxy, and the registry endpoint are candidates
7
- * (stigmer/stigmer#468). This module is the one place that answers "is this
8
- * HTTP failure worth retrying?", the `fetch` twin of grpc-retry.ts: it holds
9
- * the classification policy only, kept small and separately tested. The
10
- * bounded-backoff loop that consumes it lives with each client (the
11
- * grpc-retry/status.ts split).
6
+ * over plain `fetch` from several clients: the checkpoint saver, artifact
7
+ * storage, and the model-registry fetches (stigmer/stigmer#468). This module
8
+ * is the one place that answers "is this HTTP failure worth retrying?" — the
9
+ * `fetch` twin of grpc-retry.ts and, since #468, also owns the
10
+ * {@link fetchWithRetry} loop those clients share. That is a deliberate
11
+ * divergence from grpc-retry.ts's policy-only split: the gRPC side has a
12
+ * single consumer whose loop is interwoven with persist-specific logic
13
+ * (status.ts), while the HTTP side grew three consumers needing a
14
+ * byte-identical loop — per-client copies were the drift risk, not the seam.
12
15
  *
13
16
  * The policy is grounded in what the proxy actually emits, not HTTP folklore.
14
17
  * CheckpointerProxyController returns exactly four deterministic errors —
@@ -17,7 +20,10 @@
17
20
  * reach the runner only as 5xx (Spring's error handler, ingress 502-504),
18
21
  * 408/429 (infrastructure between runner and service), or as network-level
19
22
  * fetch rejections. 401/403 stay terminal deliberately: retrying cannot fix
20
- * an expired or rejected credential.
23
+ * an expired or rejected credential. The same classification holds for the
24
+ * presigned R2 URLs artifact storage talks to: their deterministic failures
25
+ * (403 expired signature, 404 missing object) are 4xx, their transient ones
26
+ * are 5xx/network.
21
27
  */
22
28
 
23
29
  /**
@@ -48,3 +54,86 @@ export function isRetryableFetchError(err: unknown): boolean {
48
54
  if (err instanceof TypeError) return true;
49
55
  return err instanceof Error && err.name === "TimeoutError";
50
56
  }
57
+
58
+ /**
59
+ * A fully-resolved retry policy for {@link fetchWithRetry}. Every field is
60
+ * required on purpose: defaults differ per client (the checkpointer and the
61
+ * presign endpoints run 30 s requests, artifact transfers 120 s, registry
62
+ * fetches 10 s), so each client resolves its own defaults once and this
63
+ * module never guesses.
64
+ */
65
+ export interface FetchRetryPolicy {
66
+ /** Client name for retry logs, e.g. "HttpCheckpointSaver". */
67
+ readonly label: string;
68
+ /** Delay before the first retry (ms); doubles per attempt via the factor. */
69
+ readonly baseDelayMs: number;
70
+ readonly backoffFactor: number;
71
+ /** Retry attempts after the initial one. */
72
+ readonly maxRetries: number;
73
+ /**
74
+ * Milliseconds before an in-flight request is aborted and the attempt is
75
+ * classified retryable. Without it a hung connection (the realistic
76
+ * unplanned-pod-kill symptom) stalls forever and the retry never engages.
77
+ */
78
+ readonly requestTimeoutMs: number;
79
+ /** Injectable delay for tests; defaults to a real setTimeout sleep. */
80
+ readonly delayFn?: (ms: number) => Promise<void>;
81
+ }
82
+
83
+ function defaultDelay(ms: number): Promise<void> {
84
+ return new Promise((resolve) => setTimeout(resolve, ms));
85
+ }
86
+
87
+ /**
88
+ * `fetch` with bounded exponential backoff on transient failures and a
89
+ * per-request abort timeout.
90
+ *
91
+ * Composition contract: on a non-retryable status — or when the budget is
92
+ * exhausted on a retryable one — the last `Response` is RETURNED, so every
93
+ * call site keeps its own `resp.ok` / 404 handling unchanged; this wrapper
94
+ * changes when a response arrives, never what call sites do with it. It
95
+ * throws only what `fetch` itself throws: a network-level rejection that is
96
+ * non-retryable or has exhausted the budget.
97
+ */
98
+ export async function fetchWithRetry(
99
+ url: string,
100
+ init: RequestInit | undefined,
101
+ policy: FetchRetryPolicy,
102
+ ): Promise<Response> {
103
+ const delay = policy.delayFn ?? defaultDelay;
104
+ for (let attempt = 0; ; attempt++) {
105
+ const canRetry = attempt < policy.maxRetries;
106
+ let resp: Response;
107
+ try {
108
+ resp = await fetch(url, {
109
+ ...init,
110
+ signal: AbortSignal.timeout(policy.requestTimeoutMs),
111
+ });
112
+ } catch (err) {
113
+ if (canRetry && isRetryableFetchError(err)) {
114
+ await backOff(policy, delay, attempt, String(err));
115
+ continue;
116
+ }
117
+ throw err;
118
+ }
119
+ if (canRetry && isRetryableHttpStatus(resp.status)) {
120
+ await backOff(policy, delay, attempt, `HTTP ${resp.status} ${resp.statusText}`);
121
+ continue;
122
+ }
123
+ return resp;
124
+ }
125
+ }
126
+
127
+ async function backOff(
128
+ policy: FetchRetryPolicy,
129
+ delay: (ms: number) => Promise<void>,
130
+ attempt: number,
131
+ reason: string,
132
+ ): Promise<void> {
133
+ const delayMs = policy.baseDelayMs * Math.pow(policy.backoffFactor, attempt);
134
+ console.warn(
135
+ `[${policy.label}] retryable failure ` +
136
+ `(attempt ${attempt + 1}/${policy.maxRetries + 1}, retry in ${delayMs}ms): ${reason}`,
137
+ );
138
+ await delay(delayMs);
139
+ }
@@ -4,6 +4,10 @@
4
4
  * Wraps @langchain/mcp-adapters MultiServerMCPClient with:
5
5
  * - Conversion from ResolvedMcpServer[] to the SDK's Connection config
6
6
  * - Proper async cleanup via close()
7
+ * - Input-schema sanitization of unicode-invalid regex patterns
8
+ * (issue #420; semantics in shared/mcp-schema-sanitizer.ts) — a vendor
9
+ * pattern that cannot compile under the /u flag must not leave its tool
10
+ * permanently uncallable
7
11
  * - Tool filtering by each server's effective enabled_tools allow-list
8
12
  * (issue #350; semantics in shared/mcp-enabled-tools.ts) — the model only
9
13
  * ever sees the tools the agent's manifest enables
@@ -18,10 +22,10 @@
18
22
  * (HOME, LOGNAME, PATH, SHELL, TERM, USER — getDefaultEnvironment in
19
23
  * @modelcontextprotocol/sdk, merged under whatever we pass). The
20
24
  * runner's own process env is never passed: it carries runner-internal
21
- * credentials (STIGMER_TOKEN, STIGMER_RUNNER_HITL_SECRET,
22
- * CURSOR_API_KEY, LLM provider keys) that no third-party MCP subprocess
23
- * may see. A declared-empty env and an undeclared env are deliberately
24
- * equivalent — both yield the SDK base environment.
25
+ * credentials (enumerated in runner-credential-keys.ts) that no
26
+ * third-party MCP subprocess may see. A declared-empty env and an
27
+ * undeclared env are deliberately equivalent both yield the SDK base
28
+ * environment.
25
29
  *
26
30
  * The Cursor execution path does NOT use this manager — it passes MCP
27
31
  * configs directly to the Cursor SDK via toCursorMcpConfig(). This
@@ -35,6 +39,7 @@ import type { Connection } from "@langchain/mcp-adapters";
35
39
  import type { DynamicStructuredTool } from "@langchain/core/tools";
36
40
  import type { ResolvedMcpServer } from "./mcp-resolver.js";
37
41
  import { filterToolsByEnabledTools } from "./mcp-enabled-tools.js";
42
+ import { sanitizeSchemaPatterns } from "./mcp-schema-sanitizer.js";
38
43
 
39
44
  /**
40
45
  * Convert harness-agnostic ResolvedMcpServer[] into the
@@ -100,6 +105,32 @@ export async function connectMcpServers(
100
105
  const client = new MultiServerMCPClient(connectionConfig);
101
106
  const discoveredToolMap = await client.initializeConnections();
102
107
 
108
+ // Drop unicode-invalid regex patterns from every discovered tool's input
109
+ // schema BEFORE anything downstream holds a reference (issue #420):
110
+ // @langchain/core re-validates against tool.schema on every invocation,
111
+ // so one vendor pattern that cannot compile under the /u flag would
112
+ // otherwise make its tool permanently uncallable. Semantics and the
113
+ // loosen-never-tighten invariant live in shared/mcp-schema-sanitizer.ts.
114
+ for (const [slug, discovered] of Object.entries(discoveredToolMap)) {
115
+ for (const tool of discovered) {
116
+ const droppedPatterns = sanitizeSchemaPatterns(tool.schema);
117
+ if (droppedPatterns.length > 0) {
118
+ console.warn(
119
+ `[MCP] Server '${slug}' tool '${tool.name}': dropped ` +
120
+ `${droppedPatterns.length} regex pattern(s) that cannot compile ` +
121
+ `under the unicode flag — the tool stays callable, the server ` +
122
+ `still validates its own inputs (issue #420): ` +
123
+ droppedPatterns
124
+ .map((d) =>
125
+ `${d.location} ${JSON.stringify(d.pattern)}` +
126
+ (d.compilesWithoutUnicodeFlag ? "" : " (invalid without /u too)"),
127
+ )
128
+ .join(", "),
129
+ );
130
+ }
131
+ }
132
+ }
133
+
103
134
  // Enforce each server's effective enabled_tools allow-list (issue #350)
104
135
  // HERE, before anything downstream sees the tools: the parent tool list,
105
136
  // the approval gate's toolServerMap, and the sub-agent McpAccess filter
@@ -0,0 +1,224 @@
1
+ /**
2
+ * Unicode-flag regex sanitization for MCP tool input schemas (issue #420).
3
+ *
4
+ * Why this exists: real-world MCP servers ship JSON-schema `pattern` values
5
+ * that are legal as plain ECMAScript regexes but invalid under the unicode
6
+ * flag (PayPal's `^https\:\/\/` — `\:` is an invalid escape under /u).
7
+ * @langchain/core validates tool input on EVERY call with
8
+ * @cfworker/json-schema, which compiles patterns via `new RegExp(p, "u")`
9
+ * at exactly two uncaught sites: the `pattern` keyword and each
10
+ * `patternProperties` key. One bad vendor pattern therefore makes the tool
11
+ * permanently uncallable on the deep-agent path — every invocation dies
12
+ * with a cryptic SyntaxError before the tool is ever reached. (The third
13
+ * /u compile site, `format: "regex"`, validates instance values inside a
14
+ * try/catch and fails gracefully — not a crash surface.)
15
+ *
16
+ * The fix: at the deep-agent connect funnel (mcp-manager.ts
17
+ * connectMcpServers), drop every pattern that cannot compile under /u.
18
+ * Dropping is deliberate — rewriting to a /u-safe equivalent would need an
19
+ * Annex-B-aware regex transpiler, disproportionate machinery for what is
20
+ * best-effort client-side validation; the MCP server still validates its
21
+ * own inputs server-side. This mirrors the loosening posture of
22
+ * @langchain/mcp-adapters' own schema pipeline (dereferenceJsonSchema →
23
+ * simplifyJsonSchemaForLLM), which already normalizes schemas for this
24
+ * exact validator.
25
+ *
26
+ * THE INVARIANT: sanitization may only ever LOOSEN validation, never
27
+ * tighten it. Dropping a `patternProperties` entry naively would violate
28
+ * this — @cfworker marks pattern-matched keys as "evaluated" and routes
29
+ * unevaluated keys into the `additionalProperties` / `unevaluatedProperties`
30
+ * checks, so under `additionalProperties: false` the drop would flip a
31
+ * crash into a false REJECTION of valid inputs. Whenever a
32
+ * `patternProperties` entry is dropped, any restrictive
33
+ * `additionalProperties` / `unevaluatedProperties` at that schema level is
34
+ * therefore relaxed too.
35
+ *
36
+ * Scope: this sanitizer serves the EXECUTION path only. Discovery
37
+ * (activities/discover-mcp-server.ts) persists tool schemas verbatim from
38
+ * listTools — deliberately: the stored schema is the vendor's truth for
39
+ * display, and discovery never invokes tools, so it cannot hit the crash.
40
+ * Do not "extend" sanitization there.
41
+ */
42
+
43
+ /** One dropped regex constraint, for the caller's log line. */
44
+ export interface DroppedPattern {
45
+ /** JSON-pointer-style path of the dropped constraint within the schema. */
46
+ location: string;
47
+ /** The pattern source that failed to compile under the unicode flag. */
48
+ pattern: string;
49
+ /**
50
+ * True when the pattern compiles WITHOUT the /u flag — vendor sloppiness
51
+ * (Annex B legacy syntax), the oss#420 class. False means the regex is
52
+ * broken outright; either way validation could only crash, so both drop.
53
+ */
54
+ compilesWithoutUnicodeFlag: boolean;
55
+ }
56
+
57
+ /** Keywords whose value is a single subschema. */
58
+ const SINGLE_SCHEMA_KEYWORDS = [
59
+ "additionalProperties",
60
+ "unevaluatedProperties",
61
+ "propertyNames",
62
+ "items",
63
+ "additionalItems",
64
+ "unevaluatedItems",
65
+ "contains",
66
+ "not",
67
+ "if",
68
+ "then",
69
+ "else",
70
+ ] as const;
71
+
72
+ /** Keywords whose value is an array of subschemas. */
73
+ const SCHEMA_ARRAY_KEYWORDS = ["anyOf", "allOf", "oneOf", "prefixItems"] as const;
74
+
75
+ /** Keywords whose value is an object mapping names to subschemas. */
76
+ const SCHEMA_MAP_KEYWORDS = [
77
+ "properties",
78
+ "patternProperties",
79
+ "dependentSchemas",
80
+ "$defs",
81
+ "definitions",
82
+ ] as const;
83
+
84
+ function compilesUnderUnicodeFlag(pattern: string): boolean {
85
+ try {
86
+ new RegExp(pattern, "u");
87
+ return true;
88
+ } catch {
89
+ return false;
90
+ }
91
+ }
92
+
93
+ function compilesWithoutUnicodeFlag(pattern: string): boolean {
94
+ try {
95
+ new RegExp(pattern);
96
+ return true;
97
+ } catch {
98
+ return false;
99
+ }
100
+ }
101
+
102
+ function isPlainObject(value: unknown): value is Record<string, unknown> {
103
+ return typeof value === "object" && value !== null && !Array.isArray(value);
104
+ }
105
+
106
+ /** JSON-pointer token escaping (RFC 6901): `~` → `~0`, `/` → `~1`. */
107
+ function pointerToken(token: string): string {
108
+ return token.replace(/~/g, "~0").replace(/\//g, "~1");
109
+ }
110
+
111
+ /**
112
+ * Drop unicode-invalid regex patterns from a JSON schema, in place.
113
+ *
114
+ * Walks only schema positions (the recursion set is @cfworker's own
115
+ * validated-keyword list), so a property literally NAMED "pattern" under
116
+ * `properties` is never touched. The walker handles composition keywords
117
+ * (anyOf/allOf/oneOf/not/if-then-else) even though the adapter's
118
+ * simplifyJsonSchemaForLLM currently strips them, and carries a visited-set
119
+ * cycle guard even though the adapter's $ref inlining currently guarantees
120
+ * an acyclic result — correctness here must not depend on another
121
+ * package's internals holding across dependency bumps.
122
+ *
123
+ * Returns the dropped constraints (empty for clean schemas — the common
124
+ * case, which this walk leaves byte-identical).
125
+ */
126
+ export function sanitizeSchemaPatterns(schema: unknown): DroppedPattern[] {
127
+ const dropped: DroppedPattern[] = [];
128
+ if (!isPlainObject(schema)) return dropped;
129
+ walkSchema(schema, "", dropped, new Set());
130
+ return dropped;
131
+ }
132
+
133
+ function recordDrop(dropped: DroppedPattern[], location: string, pattern: string): void {
134
+ dropped.push({
135
+ location,
136
+ pattern,
137
+ compilesWithoutUnicodeFlag: compilesWithoutUnicodeFlag(pattern),
138
+ });
139
+ }
140
+
141
+ function walkSchema(
142
+ node: Record<string, unknown>,
143
+ path: string,
144
+ dropped: DroppedPattern[],
145
+ seen: Set<object>,
146
+ ): void {
147
+ if (seen.has(node)) return;
148
+ seen.add(node);
149
+
150
+ if (typeof node.pattern === "string" && !compilesUnderUnicodeFlag(node.pattern)) {
151
+ recordDrop(dropped, `${path}/pattern`, node.pattern);
152
+ delete node.pattern;
153
+ }
154
+
155
+ if (isPlainObject(node.patternProperties)) {
156
+ const patternProperties = node.patternProperties;
157
+ let droppedEntry = false;
158
+ for (const key of Object.keys(patternProperties)) {
159
+ if (compilesUnderUnicodeFlag(key)) continue;
160
+ recordDrop(dropped, `${path}/patternProperties/${pointerToken(key)}`, key);
161
+ delete patternProperties[key];
162
+ droppedEntry = true;
163
+ }
164
+ if (droppedEntry) {
165
+ // Never-tighten: keys the dropped pattern used to match would now
166
+ // fall through to these checks and could be falsely rejected.
167
+ if (node.additionalProperties !== undefined && node.additionalProperties !== true) {
168
+ delete node.additionalProperties;
169
+ }
170
+ if (node.unevaluatedProperties !== undefined && node.unevaluatedProperties !== true) {
171
+ delete node.unevaluatedProperties;
172
+ }
173
+ if (Object.keys(patternProperties).length === 0) {
174
+ delete node.patternProperties;
175
+ }
176
+ }
177
+ }
178
+
179
+ for (const keyword of SINGLE_SCHEMA_KEYWORDS) {
180
+ const value = node[keyword];
181
+ if (isPlainObject(value)) {
182
+ walkSchema(value, `${path}/${keyword}`, dropped, seen);
183
+ }
184
+ }
185
+
186
+ for (const keyword of SCHEMA_ARRAY_KEYWORDS) {
187
+ const value = node[keyword];
188
+ if (!Array.isArray(value)) continue;
189
+ value.forEach((entry, index) => {
190
+ if (isPlainObject(entry)) {
191
+ walkSchema(entry, `${path}/${keyword}/${index}`, dropped, seen);
192
+ }
193
+ });
194
+ }
195
+
196
+ for (const keyword of SCHEMA_MAP_KEYWORDS) {
197
+ const value = node[keyword];
198
+ if (!isPlainObject(value)) continue;
199
+ for (const [name, entry] of Object.entries(value)) {
200
+ if (isPlainObject(entry)) {
201
+ walkSchema(entry, `${path}/${keyword}/${pointerToken(name)}`, dropped, seen);
202
+ }
203
+ }
204
+ }
205
+
206
+ // Draft-07 `items` tuple form and `dependencies` schema form — both
207
+ // keywords are dual-shaped, so the typed loops above miss these arms.
208
+ if (Array.isArray(node.items)) {
209
+ node.items.forEach((entry, index) => {
210
+ if (isPlainObject(entry)) {
211
+ walkSchema(entry, `${path}/items/${index}`, dropped, seen);
212
+ }
213
+ });
214
+ }
215
+ if (isPlainObject(node.dependencies)) {
216
+ for (const [name, entry] of Object.entries(node.dependencies)) {
217
+ // Array-valued entries are dependentRequired (property names, not
218
+ // schemas) — only schema-valued entries are walked.
219
+ if (isPlainObject(entry)) {
220
+ walkSchema(entry, `${path}/dependencies/${pointerToken(name)}`, dropped, seen);
221
+ }
222
+ }
223
+ }
224
+ }
@@ -60,8 +60,8 @@ export function resolveMcpTransportPosture(
60
60
  /**
61
61
  * Assert that a resolved MCP server's transport is allowed under the given
62
62
  * posture. Applies only to user-defined McpServer resources flowing through
63
- * the resolvers — the internal datastore attachment is synthesized
64
- * separately and is already HTTP in cloud mode.
63
+ * the resolvers — the internal synthesized attachments are built
64
+ * separately and are already HTTP in cloud mode.
65
65
  *
66
66
  * @throws McpTransportError when the posture forbids stdio and the server uses it
67
67
  */