gsd-pi 2.76.0-dev.b072ebb73 → 2.76.0-dev.fe143342a

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 (200) hide show
  1. package/dist/mcp-server.d.ts +7 -0
  2. package/dist/mcp-server.js +35 -1
  3. package/dist/resource-loader.d.ts +1 -1
  4. package/dist/resource-loader.js +2 -8
  5. package/dist/resources/extensions/claude-code-cli/stream-adapter.js +66 -4
  6. package/dist/resources/extensions/gsd/auto/phases.js +4 -1
  7. package/dist/resources/extensions/gsd/auto/session.js +4 -0
  8. package/dist/resources/extensions/gsd/auto-model-selection.js +39 -13
  9. package/dist/resources/extensions/gsd/auto-start.js +39 -21
  10. package/dist/resources/extensions/gsd/auto.js +15 -12
  11. package/dist/resources/extensions/gsd/blocked-models.js +68 -0
  12. package/dist/resources/extensions/gsd/bootstrap/agent-end-recovery.js +76 -0
  13. package/dist/resources/extensions/gsd/bootstrap/db-tools.js +39 -9
  14. package/dist/resources/extensions/gsd/bootstrap/exec-tools.js +93 -0
  15. package/dist/resources/extensions/gsd/bootstrap/register-extension.js +2 -0
  16. package/dist/resources/extensions/gsd/bootstrap/register-hooks.js +35 -0
  17. package/dist/resources/extensions/gsd/compaction-snapshot.js +121 -0
  18. package/dist/resources/extensions/gsd/complexity-classifier.js +5 -3
  19. package/dist/resources/extensions/gsd/error-classifier.js +31 -3
  20. package/dist/resources/extensions/gsd/exec-history.js +120 -0
  21. package/dist/resources/extensions/gsd/exec-sandbox.js +258 -0
  22. package/dist/resources/extensions/gsd/gsd-db.js +62 -4
  23. package/dist/resources/extensions/gsd/init-wizard.js +15 -1
  24. package/dist/resources/extensions/gsd/key-manager.js +6 -0
  25. package/dist/resources/extensions/gsd/pre-execution-checks.js +13 -3
  26. package/dist/resources/extensions/gsd/preferences-types.js +9 -0
  27. package/dist/resources/extensions/gsd/preferences-validation.js +83 -0
  28. package/dist/resources/extensions/gsd/preferences.js +17 -17
  29. package/dist/resources/extensions/gsd/prompt-loader.js +22 -7
  30. package/dist/resources/extensions/gsd/safety/file-change-validator.js +1 -1
  31. package/dist/resources/extensions/gsd/tools/exec-search-tool.js +59 -0
  32. package/dist/resources/extensions/gsd/tools/exec-tool.js +126 -0
  33. package/dist/resources/extensions/gsd/tools/resume-tool.js +23 -0
  34. package/dist/resources/extensions/gsd/workflow-mcp.js +3 -0
  35. package/dist/resources/extensions/search-the-web/command-search-provider.js +5 -4
  36. package/dist/resources/extensions/search-the-web/native-search.js +45 -13
  37. package/dist/tsconfig.extensions.tsbuildinfo +1 -1
  38. package/dist/web/standalone/.next/BUILD_ID +1 -1
  39. package/dist/web/standalone/.next/app-path-routes-manifest.json +8 -8
  40. package/dist/web/standalone/.next/build-manifest.json +2 -2
  41. package/dist/web/standalone/.next/prerender-manifest.json +3 -3
  42. package/dist/web/standalone/.next/required-server-files.json +1 -1
  43. package/dist/web/standalone/.next/server/app/_global-error.html +1 -1
  44. package/dist/web/standalone/.next/server/app/_global-error.rsc +1 -1
  45. package/dist/web/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  46. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
  47. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error.segment.rsc +1 -1
  48. package/dist/web/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  49. package/dist/web/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  50. package/dist/web/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  51. package/dist/web/standalone/.next/server/app/_not-found.html +1 -1
  52. package/dist/web/standalone/.next/server/app/_not-found.rsc +1 -1
  53. package/dist/web/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +1 -1
  54. package/dist/web/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
  55. package/dist/web/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +1 -1
  56. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
  57. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
  58. package/dist/web/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
  59. package/dist/web/standalone/.next/server/app/index.html +1 -1
  60. package/dist/web/standalone/.next/server/app/index.rsc +1 -1
  61. package/dist/web/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +1 -1
  62. package/dist/web/standalone/.next/server/app/index.segments/_full.segment.rsc +1 -1
  63. package/dist/web/standalone/.next/server/app/index.segments/_head.segment.rsc +1 -1
  64. package/dist/web/standalone/.next/server/app/index.segments/_index.segment.rsc +1 -1
  65. package/dist/web/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
  66. package/dist/web/standalone/.next/server/app-paths-manifest.json +8 -8
  67. package/dist/web/standalone/.next/server/middleware-build-manifest.js +1 -1
  68. package/dist/web/standalone/.next/server/middleware-manifest.json +5 -5
  69. package/dist/web/standalone/.next/server/pages/404.html +1 -1
  70. package/dist/web/standalone/.next/server/pages/500.html +1 -1
  71. package/dist/web/standalone/.next/server/server-reference-manifest.json +1 -1
  72. package/dist/web/standalone/server.js +1 -1
  73. package/package.json +1 -1
  74. package/packages/mcp-server/dist/workflow-tools.d.ts.map +1 -1
  75. package/packages/mcp-server/dist/workflow-tools.js +64 -25
  76. package/packages/mcp-server/dist/workflow-tools.js.map +1 -1
  77. package/packages/mcp-server/src/workflow-tools.test.ts +146 -1
  78. package/packages/mcp-server/src/workflow-tools.ts +84 -43
  79. package/packages/mcp-server/tsconfig.tsbuildinfo +1 -1
  80. package/packages/pi-ai/dist/providers/openai-completions.d.ts.map +1 -1
  81. package/packages/pi-ai/dist/providers/openai-completions.js +60 -15
  82. package/packages/pi-ai/dist/providers/openai-completions.js.map +1 -1
  83. package/packages/pi-ai/dist/providers/think-tag-parser.d.ts +17 -0
  84. package/packages/pi-ai/dist/providers/think-tag-parser.d.ts.map +1 -0
  85. package/packages/pi-ai/dist/providers/think-tag-parser.js +75 -0
  86. package/packages/pi-ai/dist/providers/think-tag-parser.js.map +1 -0
  87. package/packages/pi-ai/dist/providers/think-tag-parser.test.d.ts +2 -0
  88. package/packages/pi-ai/dist/providers/think-tag-parser.test.d.ts.map +1 -0
  89. package/packages/pi-ai/dist/providers/think-tag-parser.test.js +41 -0
  90. package/packages/pi-ai/dist/providers/think-tag-parser.test.js.map +1 -0
  91. package/packages/pi-ai/src/providers/openai-completions.ts +57 -16
  92. package/packages/pi-ai/src/providers/think-tag-parser.test.ts +44 -0
  93. package/packages/pi-ai/src/providers/think-tag-parser.ts +94 -0
  94. package/packages/pi-ai/tsconfig.tsbuildinfo +1 -1
  95. package/packages/pi-coding-agent/dist/core/model-discovery.d.ts +3 -1
  96. package/packages/pi-coding-agent/dist/core/model-discovery.d.ts.map +1 -1
  97. package/packages/pi-coding-agent/dist/core/model-discovery.js +92 -12
  98. package/packages/pi-coding-agent/dist/core/model-discovery.js.map +1 -1
  99. package/packages/pi-coding-agent/dist/core/model-discovery.test.js +16 -1
  100. package/packages/pi-coding-agent/dist/core/model-discovery.test.js.map +1 -1
  101. package/packages/pi-coding-agent/dist/core/model-registry-discovery.test.js +61 -1
  102. package/packages/pi-coding-agent/dist/core/model-registry-discovery.test.js.map +1 -1
  103. package/packages/pi-coding-agent/dist/core/model-registry.d.ts +5 -0
  104. package/packages/pi-coding-agent/dist/core/model-registry.d.ts.map +1 -1
  105. package/packages/pi-coding-agent/dist/core/model-registry.js +76 -10
  106. package/packages/pi-coding-agent/dist/core/model-registry.js.map +1 -1
  107. package/packages/pi-coding-agent/dist/core/redact-secrets.d.ts +2 -0
  108. package/packages/pi-coding-agent/dist/core/redact-secrets.d.ts.map +1 -0
  109. package/packages/pi-coding-agent/dist/core/redact-secrets.js +49 -0
  110. package/packages/pi-coding-agent/dist/core/redact-secrets.js.map +1 -0
  111. package/packages/pi-coding-agent/dist/core/redact-secrets.test.d.ts +2 -0
  112. package/packages/pi-coding-agent/dist/core/redact-secrets.test.d.ts.map +1 -0
  113. package/packages/pi-coding-agent/dist/core/redact-secrets.test.js +67 -0
  114. package/packages/pi-coding-agent/dist/core/redact-secrets.test.js.map +1 -0
  115. package/packages/pi-coding-agent/dist/core/session-manager.d.ts.map +1 -1
  116. package/packages/pi-coding-agent/dist/core/session-manager.js +9 -5
  117. package/packages/pi-coding-agent/dist/core/session-manager.js.map +1 -1
  118. package/packages/pi-coding-agent/dist/core/session-manager.test.js +25 -1
  119. package/packages/pi-coding-agent/dist/core/session-manager.test.js.map +1 -1
  120. package/packages/pi-coding-agent/dist/modes/interactive/components/chat-frame.d.ts +1 -1
  121. package/packages/pi-coding-agent/dist/modes/interactive/components/chat-frame.d.ts.map +1 -1
  122. package/packages/pi-coding-agent/dist/modes/interactive/components/chat-frame.js +5 -4
  123. package/packages/pi-coding-agent/dist/modes/interactive/components/chat-frame.js.map +1 -1
  124. package/packages/pi-coding-agent/dist/modes/interactive/components/provider-manager.d.ts.map +1 -1
  125. package/packages/pi-coding-agent/dist/modes/interactive/components/provider-manager.js +13 -7
  126. package/packages/pi-coding-agent/dist/modes/interactive/components/provider-manager.js.map +1 -1
  127. package/packages/pi-coding-agent/dist/modes/interactive/components/skill-invocation-message.d.ts +7 -6
  128. package/packages/pi-coding-agent/dist/modes/interactive/components/skill-invocation-message.d.ts.map +1 -1
  129. package/packages/pi-coding-agent/dist/modes/interactive/components/skill-invocation-message.js +29 -21
  130. package/packages/pi-coding-agent/dist/modes/interactive/components/skill-invocation-message.js.map +1 -1
  131. package/packages/pi-coding-agent/src/core/model-discovery.test.ts +19 -0
  132. package/packages/pi-coding-agent/src/core/model-discovery.ts +99 -12
  133. package/packages/pi-coding-agent/src/core/model-registry-discovery.test.ts +75 -0
  134. package/packages/pi-coding-agent/src/core/model-registry.ts +86 -10
  135. package/packages/pi-coding-agent/src/core/redact-secrets.test.ts +86 -0
  136. package/packages/pi-coding-agent/src/core/redact-secrets.ts +58 -0
  137. package/packages/pi-coding-agent/src/core/session-manager.test.ts +36 -1
  138. package/packages/pi-coding-agent/src/core/session-manager.ts +9 -5
  139. package/packages/pi-coding-agent/src/modes/interactive/components/chat-frame.ts +6 -6
  140. package/packages/pi-coding-agent/src/modes/interactive/components/provider-manager.ts +16 -7
  141. package/packages/pi-coding-agent/src/modes/interactive/components/skill-invocation-message.ts +36 -22
  142. package/packages/pi-coding-agent/tsconfig.tsbuildinfo +1 -1
  143. package/scripts/link-workspace-packages.cjs +1 -0
  144. package/src/resources/extensions/claude-code-cli/stream-adapter.ts +67 -4
  145. package/src/resources/extensions/claude-code-cli/tests/stream-adapter.test.ts +137 -2
  146. package/src/resources/extensions/gsd/auto/loop-deps.ts +1 -0
  147. package/src/resources/extensions/gsd/auto/phases.ts +4 -0
  148. package/src/resources/extensions/gsd/auto/session.ts +7 -1
  149. package/src/resources/extensions/gsd/auto-model-selection.ts +50 -12
  150. package/src/resources/extensions/gsd/auto-start.ts +40 -22
  151. package/src/resources/extensions/gsd/auto.ts +15 -12
  152. package/src/resources/extensions/gsd/blocked-models.ts +98 -0
  153. package/src/resources/extensions/gsd/bootstrap/agent-end-recovery.ts +97 -0
  154. package/src/resources/extensions/gsd/bootstrap/db-tools.ts +40 -9
  155. package/src/resources/extensions/gsd/bootstrap/exec-tools.ts +109 -0
  156. package/src/resources/extensions/gsd/bootstrap/register-extension.ts +2 -0
  157. package/src/resources/extensions/gsd/bootstrap/register-hooks.ts +36 -0
  158. package/src/resources/extensions/gsd/compaction-snapshot.ts +165 -0
  159. package/src/resources/extensions/gsd/complexity-classifier.ts +5 -3
  160. package/src/resources/extensions/gsd/error-classifier.ts +36 -3
  161. package/src/resources/extensions/gsd/exec-history.ts +153 -0
  162. package/src/resources/extensions/gsd/exec-sandbox.ts +326 -0
  163. package/src/resources/extensions/gsd/gsd-db.ts +68 -4
  164. package/src/resources/extensions/gsd/init-wizard.ts +15 -1
  165. package/src/resources/extensions/gsd/key-manager.ts +6 -0
  166. package/src/resources/extensions/gsd/pre-execution-checks.ts +13 -3
  167. package/src/resources/extensions/gsd/preferences-types.ts +38 -0
  168. package/src/resources/extensions/gsd/preferences-validation.ts +79 -0
  169. package/src/resources/extensions/gsd/preferences.ts +17 -17
  170. package/src/resources/extensions/gsd/prompt-loader.ts +30 -7
  171. package/src/resources/extensions/gsd/safety/file-change-validator.ts +1 -1
  172. package/src/resources/extensions/gsd/tests/auto-model-selection.test.ts +12 -0
  173. package/src/resources/extensions/gsd/tests/auto-start-model-capture.test.ts +33 -3
  174. package/src/resources/extensions/gsd/tests/auto-thinking-restore.test.ts +38 -0
  175. package/src/resources/extensions/gsd/tests/blocked-models.test.ts +98 -0
  176. package/src/resources/extensions/gsd/tests/compaction-snapshot.test.ts +123 -0
  177. package/src/resources/extensions/gsd/tests/complexity-classifier.test.ts +3 -3
  178. package/src/resources/extensions/gsd/tests/exec-history.test.ts +124 -0
  179. package/src/resources/extensions/gsd/tests/exec-sandbox.test.ts +210 -0
  180. package/src/resources/extensions/gsd/tests/file-change-validator.test.ts +20 -0
  181. package/src/resources/extensions/gsd/tests/gsd-db.test.ts +151 -0
  182. package/src/resources/extensions/gsd/tests/init-wizard.test.ts +27 -0
  183. package/src/resources/extensions/gsd/tests/isolation-none-branch-guard.test.ts +1 -1
  184. package/src/resources/extensions/gsd/tests/key-manager.test.ts +7 -0
  185. package/src/resources/extensions/gsd/tests/pre-exec-backtick-strip.test.ts +14 -0
  186. package/src/resources/extensions/gsd/tests/pre-execution-checks.test.ts +19 -0
  187. package/src/resources/extensions/gsd/tests/preferences.test.ts +110 -0
  188. package/src/resources/extensions/gsd/tests/prompt-loader-extension-dir.test.ts +49 -0
  189. package/src/resources/extensions/gsd/tests/provider-errors.test.ts +91 -0
  190. package/src/resources/extensions/gsd/tests/save-gate-result-render.test.ts +95 -0
  191. package/src/resources/extensions/gsd/tests/zombie-gsd-state.test.ts +3 -1
  192. package/src/resources/extensions/gsd/tools/exec-search-tool.ts +81 -0
  193. package/src/resources/extensions/gsd/tools/exec-tool.ts +183 -0
  194. package/src/resources/extensions/gsd/tools/resume-tool.ts +40 -0
  195. package/src/resources/extensions/gsd/workflow-logger.ts +2 -1
  196. package/src/resources/extensions/gsd/workflow-mcp.ts +3 -0
  197. package/src/resources/extensions/search-the-web/command-search-provider.ts +5 -4
  198. package/src/resources/extensions/search-the-web/native-search.ts +48 -12
  199. /package/dist/web/standalone/.next/static/{pBwmOoye64ZrRp-_rf0v1 → n21VtX2hZlkpdEUO_nU4z}/_buildManifest.js +0 -0
  200. /package/dist/web/standalone/.next/static/{pBwmOoye64ZrRp-_rf0v1 → n21VtX2hZlkpdEUO_nU4z}/_ssgManifest.js +0 -0
@@ -34,6 +34,7 @@ const packageMap = {
34
34
  'pi-coding-agent': { scope: '@gsd', name: 'pi-coding-agent' },
35
35
  'pi-tui': { scope: '@gsd', name: 'pi-tui' },
36
36
  'rpc-client': { scope: '@gsd-build', name: 'rpc-client' },
37
+ 'mcp-server': { scope: '@gsd-build', name: 'mcp-server' },
37
38
  }
38
39
 
39
40
  for (const scopeDir of Object.values(scopeDirs)) {
@@ -867,6 +867,69 @@ function normalizeToolResultContent(content: unknown): ExternalToolResultContent
867
867
  return blocks.length > 0 ? blocks : [{ type: "text", text: "" }];
868
868
  }
869
869
 
870
+ /**
871
+ * Extract a `details` payload from an MCP tool-result block.
872
+ *
873
+ * MCP's `CallToolResult` carries structured data in `structuredContent` — the
874
+ * protocol's supported channel for non-text payloads. Claude Code's synthetic
875
+ * user message may surface that field in one of two shapes depending on SDK
876
+ * version: as a sibling on the `mcp_tool_result` block itself, or as a
877
+ * dedicated content sub-block with `type: "structuredContent"`. Snake-case
878
+ * (`structured_content`) is accepted defensively in case a transport hop
879
+ * rewrites casing. All other shapes fall back to an empty object so callers
880
+ * can rely on `details` being present.
881
+ */
882
+ function extractStructuredDetailsFromBlock(block: Record<string, unknown>): Record<string, unknown> | undefined {
883
+ const sibling = block.structuredContent ?? (block as Record<string, unknown>).structured_content;
884
+ if (sibling && typeof sibling === "object" && !Array.isArray(sibling)) {
885
+ return sibling as Record<string, unknown>;
886
+ }
887
+
888
+ if (Array.isArray(block.content)) {
889
+ for (const item of block.content) {
890
+ if (!item || typeof item !== "object") continue;
891
+ const sub = item as Record<string, unknown>;
892
+ if (sub.type !== "structuredContent" && sub.type !== "structured_content") continue;
893
+ const payload = sub.structuredContent ?? sub.structured_content ?? sub.data ?? sub.value;
894
+ if (payload && typeof payload === "object" && !Array.isArray(payload)) {
895
+ return payload as Record<string, unknown>;
896
+ }
897
+ }
898
+ }
899
+
900
+ // Return undefined (not {}) when no structured payload is present, matching
901
+ // the pre-#4477 contract where `details` was nullable. An empty-object
902
+ // sentinel is truthy and breaks downstream consumers that gate on
903
+ // `if (details)`. `undefined` matches the type of the field these results
904
+ // flow into (`Record<string, unknown> | undefined`).
905
+ return undefined;
906
+ }
907
+
908
+ /**
909
+ * True for items that are MCP `structuredContent` pseudo-blocks living inside
910
+ * a tool-result `content[]` array. These blocks carry the structured payload
911
+ * (extracted separately by `extractStructuredDetailsFromBlock`) and must NOT
912
+ * leak into the visible content rendered to the user — otherwise the renderer
913
+ * stringifies the JSON pseudo-block and shows it next to the actual tool
914
+ * output. See PR #4477 review (CodeRabbit, post-fix-round).
915
+ */
916
+ function isStructuredContentPseudoBlock(item: unknown): boolean {
917
+ if (!item || typeof item !== "object") return false;
918
+ const type = (item as Record<string, unknown>).type;
919
+ return type === "structuredContent" || type === "structured_content";
920
+ }
921
+
922
+ /**
923
+ * Strip `structuredContent` pseudo-blocks from a tool-result content array
924
+ * before normalization. The structured payload is extracted via the sibling
925
+ * `structuredContent` field (or a dedicated extractor pass on the raw block);
926
+ * the visible content path must not include the pseudo-block itself.
927
+ */
928
+ function stripStructuredContentPseudoBlocks(content: unknown): unknown {
929
+ if (!Array.isArray(content)) return content;
930
+ return content.filter((item) => !isStructuredContentPseudoBlock(item));
931
+ }
932
+
870
933
  /** Extract tool result payloads from an SDK synthetic user message, keyed by tool-use ID. */
871
934
  export function extractToolResultsFromSdkUserMessage(message: SDKUserMessage): Array<{
872
935
  toolUseId: string;
@@ -890,8 +953,8 @@ export function extractToolResultsFromSdkUserMessage(message: SDKUserMessage): A
890
953
  extracted.push({
891
954
  toolUseId,
892
955
  result: {
893
- content: normalizeToolResultContent(block.content),
894
- details: {},
956
+ content: normalizeToolResultContent(stripStructuredContentPseudoBlocks(block.content)),
957
+ details: extractStructuredDetailsFromBlock(block),
895
958
  isError: block.is_error === true,
896
959
  },
897
960
  });
@@ -906,8 +969,8 @@ export function extractToolResultsFromSdkUserMessage(message: SDKUserMessage): A
906
969
  extracted.push({
907
970
  toolUseId,
908
971
  result: {
909
- content: normalizeToolResultContent(toolResult.content),
910
- details: {},
972
+ content: normalizeToolResultContent(stripStructuredContentPseudoBlocks(toolResult.content)),
973
+ details: extractStructuredDetailsFromBlock(toolResult),
911
974
  isError: toolResult.is_error === true,
912
975
  },
913
976
  });
@@ -372,13 +372,146 @@ describe("stream-adapter — Claude Code external tool results", () => {
372
372
  toolUseId: "tool-bash-1",
373
373
  result: {
374
374
  content: [{ type: "text", text: "line 1\nline 2" }],
375
- details: {},
375
+ // extractStructuredDetailsFromBlock returns undefined when no
376
+ // structured payload exists, restoring the pre-#4477 nullable
377
+ // contract (#4477 review feedback).
378
+ details: undefined,
376
379
  isError: false,
377
380
  },
378
381
  },
379
382
  ]);
380
383
  });
381
384
 
385
+ test("extractToolResultsFromSdkUserMessage reads structuredContent as a sibling field (#4472)", () => {
386
+ const message: SDKUserMessage = {
387
+ type: "user",
388
+ session_id: "sess-1",
389
+ parent_tool_use_id: "tool-mcp-1",
390
+ message: {
391
+ role: "user",
392
+ content: [
393
+ {
394
+ type: "mcp_tool_result",
395
+ tool_use_id: "tool-mcp-1",
396
+ content: [{ type: "text", text: "Gate Q3 result saved: verdict=pass" }],
397
+ is_error: false,
398
+ structuredContent: { gateId: "Q3", verdict: "pass" },
399
+ } as unknown as Record<string, unknown>,
400
+ ],
401
+ },
402
+ };
403
+
404
+ const results = extractToolResultsFromSdkUserMessage(message);
405
+ assert.deepEqual(results[0].result.details, { gateId: "Q3", verdict: "pass" });
406
+ });
407
+
408
+ test("extractToolResultsFromSdkUserMessage reads structuredContent from a content sub-block (#4472)", () => {
409
+ const message: SDKUserMessage = {
410
+ type: "user",
411
+ session_id: "sess-1",
412
+ parent_tool_use_id: "tool-mcp-2",
413
+ message: {
414
+ role: "user",
415
+ content: [
416
+ {
417
+ type: "mcp_tool_result",
418
+ tool_use_id: "tool-mcp-2",
419
+ content: [
420
+ { type: "text", text: "Gate Q4 result saved: verdict=flag" },
421
+ { type: "structuredContent", structuredContent: { gateId: "Q4", verdict: "flag" } },
422
+ ],
423
+ is_error: false,
424
+ } as unknown as Record<string, unknown>,
425
+ ],
426
+ },
427
+ };
428
+
429
+ const results = extractToolResultsFromSdkUserMessage(message);
430
+ assert.deepEqual(results[0].result.details, { gateId: "Q4", verdict: "flag" });
431
+ });
432
+
433
+ test("#4477 extractToolResultsFromSdkUserMessage does NOT leak structuredContent pseudo-blocks into visible content", () => {
434
+ // Regression: when a content sub-block carries `type: "structuredContent"`,
435
+ // it carries the structured payload (extracted separately into `details`)
436
+ // and must NOT appear in the visible `content` array — otherwise the
437
+ // renderer stringifies the JSON pseudo-block and shows it next to the
438
+ // actual tool output. See PR #4477 review (CodeRabbit, post-fix-round).
439
+ const message: SDKUserMessage = {
440
+ type: "user",
441
+ session_id: "sess-1",
442
+ parent_tool_use_id: "tool-mcp-strip",
443
+ message: {
444
+ role: "user",
445
+ content: [
446
+ {
447
+ type: "mcp_tool_result",
448
+ tool_use_id: "tool-mcp-strip",
449
+ content: [
450
+ { type: "text", text: "Gate Q5 result saved: verdict=pass" },
451
+ { type: "structuredContent", structuredContent: { gateId: "Q5", verdict: "pass" } },
452
+ { type: "text", text: "second visible line" },
453
+ // snake_case variant — also a pseudo-block; also must be stripped
454
+ { type: "structured_content", structured_content: { extra: "data" } },
455
+ ],
456
+ is_error: false,
457
+ } as unknown as Record<string, unknown>,
458
+ ],
459
+ },
460
+ };
461
+
462
+ const results = extractToolResultsFromSdkUserMessage(message);
463
+ assert.equal(results.length, 1, "should extract one result");
464
+ const result = results[0].result;
465
+
466
+ // The structured payload IS extracted to `details`.
467
+ assert.deepEqual(result.details, { gateId: "Q5", verdict: "pass" });
468
+
469
+ // The visible content has the two text blocks but NEITHER pseudo-block.
470
+ const visibleTexts = result.content.map((c: any) => c.text);
471
+ assert.deepEqual(
472
+ visibleTexts,
473
+ ["Gate Q5 result saved: verdict=pass", "second visible line"],
474
+ "visible content must include only the two text blocks; both structuredContent variants must be stripped",
475
+ );
476
+
477
+ // Belt-and-suspenders: assert no rendered text shows the JSON serialization
478
+ // of a pseudo-block. We don't check for bare keys like "gateId" or "verdict"
479
+ // because those are legitimate words in the gate-result message text. The
480
+ // regression signature would be a JSON-shaped substring that could only
481
+ // appear via stringification.
482
+ const allText = visibleTexts.join("\n");
483
+ assert.ok(
484
+ !allText.includes('"structuredContent"'),
485
+ "rendered content must not include the pseudo-block type marker as JSON text",
486
+ );
487
+ assert.ok(
488
+ !allText.includes('"structured_content"'),
489
+ "rendered content must not include the snake_case pseudo-block type marker as JSON text",
490
+ );
491
+ });
492
+
493
+ test("extractToolResultsFromSdkUserMessage accepts snake_case structured_content defensively (#4472)", () => {
494
+ const message: SDKUserMessage = {
495
+ type: "user",
496
+ session_id: "sess-1",
497
+ parent_tool_use_id: "tool-mcp-3",
498
+ message: {
499
+ role: "user",
500
+ content: [
501
+ {
502
+ type: "mcp_tool_result",
503
+ tool_use_id: "tool-mcp-3",
504
+ content: [{ type: "text", text: "ok" }],
505
+ structured_content: { operation: "save_gate_result" },
506
+ } as unknown as Record<string, unknown>,
507
+ ],
508
+ },
509
+ };
510
+
511
+ const results = extractToolResultsFromSdkUserMessage(message);
512
+ assert.deepEqual(results[0].result.details, { operation: "save_gate_result" });
513
+ });
514
+
382
515
  test("extractToolResultsFromSdkUserMessage falls back to tool_use_result", () => {
383
516
  const message: SDKUserMessage = {
384
517
  type: "user",
@@ -398,7 +531,9 @@ describe("stream-adapter — Claude Code external tool results", () => {
398
531
  toolUseId: "tool-read-1",
399
532
  result: {
400
533
  content: [{ type: "text", text: "file contents" }],
401
- details: {},
534
+ // undefined (not {}) per the restored nullable contract — see
535
+ // the analogous assertion in the tool_result test above.
536
+ details: undefined,
402
537
  isError: true,
403
538
  },
404
539
  },
@@ -213,6 +213,7 @@ export interface LoopDeps {
213
213
  retryContext?: { isRetry: boolean; previousTier?: string },
214
214
  isAutoMode?: boolean,
215
215
  sessionModelOverride?: { provider: string; id: string } | null,
216
+ autoModeStartThinkingLevel?: ReturnType<ExtensionAPI["getThinkingLevel"]> | null,
216
217
  ) => Promise<{
217
218
  routing: { tier: string; modelDowngraded: boolean } | null;
218
219
  appliedModel: { provider: string; id: string } | null;
@@ -1471,6 +1471,7 @@ export async function runUnitPhase(
1471
1471
  sidecarItem ? undefined : { isRetry, previousTier },
1472
1472
  undefined,
1473
1473
  s.manualSessionModelOverride,
1474
+ s.autoModeStartThinkingLevel,
1474
1475
  );
1475
1476
  s.currentUnitRouting =
1476
1477
  modelResult.routing as AutoSession["currentUnitRouting"];
@@ -1485,6 +1486,9 @@ export async function runUnitPhase(
1485
1486
  if (match) {
1486
1487
  const ok = await pi.setModel(match, { persist: false });
1487
1488
  if (ok) {
1489
+ if (s.autoModeStartThinkingLevel) {
1490
+ pi.setThinkingLevel(s.autoModeStartThinkingLevel);
1491
+ }
1488
1492
  s.currentUnitModel = match as AutoSession["currentUnitModel"];
1489
1493
  ctx.ui.notify(`Hook model override: ${match.provider}/${match.id}`, "info");
1490
1494
  } else {
@@ -17,7 +17,7 @@
17
17
  */
18
18
 
19
19
  import type { Api, Model } from "@gsd/pi-ai";
20
- import type { ExtensionCommandContext } from "@gsd/pi-coding-agent";
20
+ import type { ExtensionAPI, ExtensionCommandContext } from "@gsd/pi-coding-agent";
21
21
  import type { GitServiceImpl } from "../git-service.js";
22
22
  import type { CaptureEntry } from "../captures.js";
23
23
  import type { BudgetAlertLevel } from "../auto-budget.js";
@@ -40,6 +40,8 @@ export interface StartModel {
40
40
  id: string;
41
41
  }
42
42
 
43
+ export type ThinkingLevelSnapshot = ReturnType<ExtensionAPI["getThinkingLevel"]>;
44
+
43
45
  export interface PendingVerificationRetry {
44
46
  unitId: string;
45
47
  failureContext: string;
@@ -120,6 +122,8 @@ export class AutoSession {
120
122
  currentDispatchedModelId: string | null = null;
121
123
  originalModelId: string | null = null;
122
124
  originalModelProvider: string | null = null;
125
+ autoModeStartThinkingLevel: ThinkingLevelSnapshot | null = null;
126
+ originalThinkingLevel: ThinkingLevelSnapshot | null = null;
123
127
  lastBudgetAlertLevel: BudgetAlertLevel = 0;
124
128
 
125
129
  // ── Recovery ─────────────────────────────────────────────────────────────
@@ -241,6 +245,8 @@ export class AutoSession {
241
245
  this.currentDispatchedModelId = null;
242
246
  this.originalModelId = null;
243
247
  this.originalModelProvider = null;
248
+ this.autoModeStartThinkingLevel = null;
249
+ this.originalThinkingLevel = null;
244
250
  this.lastBudgetAlertLevel = 0;
245
251
 
246
252
  // Recovery
@@ -18,6 +18,7 @@ import { getSessionModelOverride } from "./session-model-override.js";
18
18
  import { logWarning } from "./workflow-logger.js";
19
19
  import { resolveUokFlags } from "./uok/flags.js";
20
20
  import { applyModelPolicyFilter } from "./uok/model-policy.js";
21
+ import { isModelBlocked } from "./blocked-models.js";
21
22
 
22
23
  export interface ModelSelectionResult {
23
24
  /** Routing metadata for metrics recording */
@@ -32,6 +33,14 @@ export interface PreferredModelConfig {
32
33
  source: "explicit" | "synthesized";
33
34
  }
34
35
 
36
+ function reapplyThinkingLevel(
37
+ pi: ExtensionAPI,
38
+ level: ReturnType<ExtensionAPI["getThinkingLevel"]> | null | undefined,
39
+ ): void {
40
+ if (!level) return;
41
+ pi.setThinkingLevel(level);
42
+ }
43
+
35
44
  export function resolvePreferredModelConfig(
36
45
  unitType: string,
37
46
  autoModeStartModel: { provider: string; id: string; flatRateCtx?: FlatRateContext } | null,
@@ -96,6 +105,8 @@ export async function selectAndApplyModel(
96
105
  isAutoMode = true,
97
106
  /** Explicit /gsd model pin captured at bootstrap for long-running auto loops. */
98
107
  sessionModelOverride?: { provider: string; id: string } | null,
108
+ /** Thinking level captured at auto-mode start and re-applied after model swaps. */
109
+ autoModeStartThinkingLevel?: ReturnType<ExtensionAPI["getThinkingLevel"]> | null,
99
110
  ): Promise<ModelSelectionResult> {
100
111
  const uokFlags = resolveUokFlags(prefs);
101
112
  const effectiveSessionModelOverride = sessionModelOverride === undefined
@@ -352,6 +363,18 @@ export async function selectAndApplyModel(
352
363
  attemptedPolicyEligible = true;
353
364
  }
354
365
 
366
+ // Skip models the provider has previously rejected for this account
367
+ // (issue #4513). The block is persisted in .gsd/runtime/blocked-models.json
368
+ // so it survives /gsd auto restarts — without this, the same dead model
369
+ // gets reselected after every restart.
370
+ if (isModelBlocked(basePath, model.provider, model.id)) {
371
+ ctx.ui.notify(
372
+ `Skipping blocked model ${model.provider}/${model.id} (provider rejected it for this account).`,
373
+ "warning",
374
+ );
375
+ continue;
376
+ }
377
+
355
378
  // Warn if the ID is ambiguous across providers
356
379
  if (!modelId.includes("/")) {
357
380
  const providers = availableModels.filter(m => m.id === modelId).map(m => m.provider);
@@ -367,6 +390,7 @@ export async function selectAndApplyModel(
367
390
  const ok = await pi.setModel(model, { persist: false });
368
391
  if (ok) {
369
392
  appliedModel = model;
393
+ reapplyThinkingLevel(pi, autoModeStartThinkingLevel);
370
394
 
371
395
  // ADR-005: Adjust active tool set for the selected model's provider capabilities.
372
396
  // Hard-filter incompatible tools, then let extensions override via adjust_tool_set hook.
@@ -425,19 +449,33 @@ export async function selectAndApplyModel(
425
449
  // No model preference for this unit type — re-apply the model captured
426
450
  // at auto-mode start to prevent bleed from shared global settings.json (#650).
427
451
  const availableModels = ctx.modelRegistry.getAvailable();
428
- const startModel = availableModels.find(
429
- m => m.provider === autoModeStartModel.provider && m.id === autoModeStartModel.id,
430
- );
431
- if (startModel) {
432
- const ok = await pi.setModel(startModel, { persist: false });
433
- if (!ok) {
434
- const byId = availableModels.find(m => m.id === autoModeStartModel.id);
435
- if (byId) {
436
- const fallbackOk = await pi.setModel(byId, { persist: false });
437
- if (fallbackOk) appliedModel = byId;
452
+ const startBlocked = isModelBlocked(basePath, autoModeStartModel.provider, autoModeStartModel.id);
453
+ if (startBlocked) {
454
+ ctx.ui.notify(
455
+ `Auto-mode start model ${autoModeStartModel.provider}/${autoModeStartModel.id} is blocked for this account. Using current session model instead.`,
456
+ "warning",
457
+ );
458
+ } else {
459
+ const startModel = availableModels.find(
460
+ m => m.provider === autoModeStartModel.provider && m.id === autoModeStartModel.id,
461
+ );
462
+ if (startModel) {
463
+ const ok = await pi.setModel(startModel, { persist: false });
464
+ if (!ok) {
465
+ const byId = availableModels.find(
466
+ m => m.id === autoModeStartModel.id && !isModelBlocked(basePath, m.provider, m.id),
467
+ );
468
+ if (byId) {
469
+ const fallbackOk = await pi.setModel(byId, { persist: false });
470
+ if (fallbackOk) {
471
+ appliedModel = byId;
472
+ reapplyThinkingLevel(pi, autoModeStartThinkingLevel);
473
+ }
474
+ }
475
+ } else {
476
+ appliedModel = startModel;
477
+ reapplyThinkingLevel(pi, autoModeStartThinkingLevel);
438
478
  }
439
- } else {
440
- appliedModel = startModel;
441
479
  }
442
480
  }
443
481
  }
@@ -60,7 +60,7 @@ import { initRoutingHistory } from "./routing-history.js";
60
60
  import { restoreHookState, resetHookState } from "./post-unit-hooks.js";
61
61
  import { resetProactiveHealing, setLevelChangeCallback } from "./doctor-proactive.js";
62
62
  import { snapshotSkills } from "./skill-discovery.js";
63
- import { isDbAvailable, getMilestone, openDatabase } from "./gsd-db.js";
63
+ import { isDbAvailable, getMilestone, openDatabase, getDbStatus } from "./gsd-db.js";
64
64
  import { hideFooter } from "./auto-dashboard.js";
65
65
  import {
66
66
  debugLog,
@@ -92,7 +92,7 @@ import type { WorktreeResolver } from "./worktree-resolver.js";
92
92
  import { getSessionModelOverride } from "./session-model-override.js";
93
93
 
94
94
  export interface BootstrapDeps {
95
- shouldUseWorktreeIsolation: () => boolean;
95
+ shouldUseWorktreeIsolation: (basePath?: string) => boolean;
96
96
  registerSigtermHandler: (basePath: string) => void;
97
97
  lockBase: () => string;
98
98
  buildResolver: () => WorktreeResolver;
@@ -273,8 +273,8 @@ export async function bootstrapAutoSession(
273
273
  //
274
274
  // Precedence:
275
275
  // 1) Explicit session override via /gsd model (this session)
276
- // 2) GSD model preferences from PREFERENCES.md (validated against live auth)
277
- // 3) Current session model from settings/session restore (if provider ready)
276
+ // 2) Current session model from settings/session restore (if provider ready)
277
+ // 3) GSD model preferences from PREFERENCES.md (validated against live auth)
278
278
  //
279
279
  // This preserves #3517 defaults while honoring explicit runtime model
280
280
  // selection for subsequent /gsd runs in the same session.
@@ -314,11 +314,14 @@ export async function bootstrapAutoSession(
314
314
  }
315
315
  const sessionModelReady =
316
316
  ctx.model && ctx.modelRegistry.isProviderRequestReady(ctx.model.provider);
317
+ const currentSessionModel = (sessionModelReady && ctx.model)
318
+ ? { provider: ctx.model.provider, id: ctx.model.id }
319
+ : null;
320
+ const startThinkingSnapshot = pi.getThinkingLevel();
317
321
  const startModelSnapshot = manualSessionOverride
322
+ ?? currentSessionModel
318
323
  ?? validatedPreferredModel
319
- ?? (sessionModelReady && ctx.model
320
- ? { provider: ctx.model.provider, id: ctx.model.id }
321
- : null);
324
+ ?? null;
322
325
 
323
326
  try {
324
327
  // Validate GSD_PROJECT_ID early so the user gets immediate feedback
@@ -340,7 +343,7 @@ export async function bootstrapAutoSession(
340
343
  const hasLocalGit = existsSync(join(base, ".git"));
341
344
  if (!hasLocalGit || isInheritedRepo(base)) {
342
345
  const mainBranch =
343
- loadEffectiveGSDPreferences()?.preferences?.git?.main_branch || "main";
346
+ loadEffectiveGSDPreferences(base)?.preferences?.git?.main_branch || "main";
344
347
  nativeInit(base, mainBranch);
345
348
  }
346
349
 
@@ -358,7 +361,7 @@ export async function bootstrapAutoSession(
358
361
  // Ensure .gitignore has baseline patterns.
359
362
  // ensureGitignore checks for git-tracked .gsd/ files and skips the
360
363
  // ".gsd" pattern if the project intentionally tracks .gsd/ in git.
361
- const gitPrefs = loadEffectiveGSDPreferences()?.preferences?.git;
364
+ const gitPrefs = loadEffectiveGSDPreferences(base)?.preferences?.git;
362
365
  const manageGitignore = gitPrefs?.manage_gitignore;
363
366
  ensureGitignore(base, { manageGitignore });
364
367
  if (manageGitignore !== false) untrackRuntimeFiles(base);
@@ -387,7 +390,7 @@ export async function bootstrapAutoSession(
387
390
  // Initialize GitServiceImpl
388
391
  s.gitService = new GitServiceImpl(
389
392
  s.basePath,
390
- loadEffectiveGSDPreferences()?.preferences?.git ?? {},
393
+ loadEffectiveGSDPreferences(base)?.preferences?.git ?? {},
391
394
  );
392
395
 
393
396
  // ── Debug mode ──
@@ -431,7 +434,7 @@ export async function bootstrapAutoSession(
431
434
  // was lost due to session ending between completion and teardown.
432
435
  // Must run after DB open and before worktree entry.
433
436
  try {
434
- const auditResult = auditOrphanedMilestoneBranches(base, getIsolationMode());
437
+ const auditResult = auditOrphanedMilestoneBranches(base, getIsolationMode(base));
435
438
  for (const msg of auditResult.recovered) {
436
439
  ctx.ui.notify(`Orphan audit: ${msg}`, "info");
437
440
  }
@@ -451,7 +454,7 @@ export async function bootstrapAutoSession(
451
454
  // Stale worktree state recovery (#654)
452
455
  if (
453
456
  state.activeMilestone &&
454
- shouldUseWorktreeIsolation() &&
457
+ shouldUseWorktreeIsolation(base) &&
455
458
  !detectWorktreeName(base)
456
459
  ) {
457
460
  const wtPath = getAutoWorktreePath(base, state.activeMilestone.id);
@@ -469,7 +472,7 @@ export async function bootstrapAutoSession(
469
472
  if (
470
473
  state.activeMilestone &&
471
474
  (state.phase === "pre-planning" || state.phase === "complete") &&
472
- getIsolationMode() !== "none" &&
475
+ getIsolationMode(base) !== "none" &&
473
476
  !detectWorktreeName(base) &&
474
477
  !base.includes(`${pathSep}.gsd${pathSep}worktrees${pathSep}`)
475
478
  ) {
@@ -664,15 +667,16 @@ export async function bootstrapAutoSession(
664
667
  s.pendingQuickTasks = [];
665
668
  s.currentUnit = null;
666
669
  s.currentMilestoneId = state.activeMilestone?.id ?? null;
667
- s.originalModelId = ctx.model?.id ?? null;
668
- s.originalModelProvider = ctx.model?.provider ?? null;
670
+ s.originalModelId = startModelSnapshot?.id ?? ctx.model?.id ?? null;
671
+ s.originalModelProvider = startModelSnapshot?.provider ?? ctx.model?.provider ?? null;
672
+ s.originalThinkingLevel = startThinkingSnapshot ?? null;
669
673
 
670
674
  // Register SIGTERM handler
671
675
  registerSigtermHandler(base);
672
676
 
673
677
  // Capture integration branch
674
678
  if (s.currentMilestoneId) {
675
- if (getIsolationMode() !== "none") {
679
+ if (getIsolationMode(base) !== "none") {
676
680
  captureIntegrationBranch(base, s.currentMilestoneId);
677
681
  }
678
682
  setActiveMilestoneId(base, s.currentMilestoneId);
@@ -681,7 +685,7 @@ export async function bootstrapAutoSession(
681
685
  // Guard against stale milestone branch when isolation:none (#3613).
682
686
  // A prior session with isolation:branch/worktree may have left HEAD on
683
687
  // milestone/<MID>. Auto-checkout back to the integration branch.
684
- if (getIsolationMode() === "none" && nativeIsRepo(base)) {
688
+ if (getIsolationMode(base) === "none" && nativeIsRepo(base)) {
685
689
  try {
686
690
  const currentBranch = nativeGetCurrentBranch(base);
687
691
  if (currentBranch.startsWith("milestone/")) {
@@ -712,7 +716,7 @@ export async function bootstrapAutoSession(
712
716
 
713
717
  if (
714
718
  s.currentMilestoneId &&
715
- getIsolationMode() !== "none" &&
719
+ getIsolationMode(base) !== "none" &&
716
720
  !detectWorktreeName(base) &&
717
721
  !isUnderGsdWorktrees(base)
718
722
  ) {
@@ -758,9 +762,22 @@ export async function bootstrapAutoSession(
758
762
  // call returns "db_unavailable", triggering artifact-retry which
759
763
  // re-dispatches the same task — producing an infinite loop (#2419).
760
764
  if (existsSync(gsdDbPath) && !isDbAvailable()) {
765
+ const dbStatus = getDbStatus();
766
+ const phaseHint = dbStatus.lastPhase === "open"
767
+ ? "The database file could not be opened"
768
+ : dbStatus.lastPhase === "initSchema"
769
+ ? "The database schema could not be initialized"
770
+ : dbStatus.lastPhase === "vacuum-recovery"
771
+ ? "Corruption recovery (VACUUM) failed"
772
+ : dbStatus.attempted
773
+ ? "The database could not be opened (phase unknown)"
774
+ : "The database provider could not be loaded";
775
+ const errorDetail = dbStatus.lastError ? ` (${dbStatus.lastError.message})` : "";
776
+ const providerHint = dbStatus.provider
777
+ ? ` Provider: ${dbStatus.provider}.`
778
+ : " No SQLite provider available — check Node >= 22 or install better-sqlite3.";
761
779
  ctx.ui.notify(
762
- "SQLite database exists but failed to open. Auto-mode cannot proceed without a working database provider. " +
763
- "Check for corrupt gsd.db or missing native SQLite bindings.",
780
+ `SQLite database exists but failed to open: ${gsdDbPath}. ${phaseHint}${errorDetail}.${providerHint}`,
764
781
  "error",
765
782
  );
766
783
  return releaseLockAndReturn();
@@ -779,6 +796,7 @@ export async function bootstrapAutoSession(
779
796
  id: startModelSnapshot.id,
780
797
  };
781
798
  }
799
+ s.autoModeStartThinkingLevel = startThinkingSnapshot ?? null;
782
800
  s.manualSessionModelOverride = manualSessionOverride ?? null;
783
801
 
784
802
  // Apply worker model override from parallel orchestrator (#worker-model).
@@ -801,7 +819,7 @@ export async function bootstrapAutoSession(
801
819
  }
802
820
 
803
821
  // Snapshot installed skills
804
- if (resolveSkillDiscoveryMode() !== "off") {
822
+ if (resolveSkillDiscoveryMode(base) !== "off") {
805
823
  snapshotSkills();
806
824
  }
807
825
 
@@ -835,7 +853,7 @@ export async function bootstrapAutoSession(
835
853
  // FlatRateContext used by selectAndApplyModel so user-declared
836
854
  // flat-rate providers and externalCli auto-detection are respected.
837
855
  const { isFlatRateProvider, buildFlatRateContext } = await import("./auto-model-selection.js");
838
- const bannerPrefs = loadEffectiveGSDPreferences()?.preferences;
856
+ const bannerPrefs = loadEffectiveGSDPreferences(base)?.preferences;
839
857
  const effectiveProvider = s.autoModeStartModel?.provider ?? ctx.model?.provider;
840
858
  const effectivelyEnabled = routingConfig.enabled
841
859
  && (routingConfig.allow_flat_rate_providers