skedyul 1.7.8 → 1.7.9

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.
package/dist/.build-stamp CHANGED
@@ -1 +1 @@
1
- 1785985393398
1
+ 1785987631012
package/dist/cli/index.js CHANGED
@@ -4658,6 +4658,39 @@ async function handleMcpBatchRoute(req, ctx) {
4658
4658
  }
4659
4659
  };
4660
4660
  }
4661
+ function isBatchOperationFailure(result) {
4662
+ return result != null && typeof result === "object" && result.success === false && typeof result.error === "object" && result.error != null;
4663
+ }
4664
+ function toStringEnv(env) {
4665
+ const out = {};
4666
+ for (const [key, value] of Object.entries(env)) {
4667
+ if (value !== void 0) {
4668
+ out[key] = value;
4669
+ }
4670
+ }
4671
+ return out;
4672
+ }
4673
+ function stripBillingFields(result) {
4674
+ const {
4675
+ billing: _billing,
4676
+ success: _success,
4677
+ error: _error,
4678
+ retry: _retry,
4679
+ ...domain
4680
+ } = result;
4681
+ return domain;
4682
+ }
4683
+ function softFailureResponse(error, retry, billing) {
4684
+ return {
4685
+ status: 200,
4686
+ body: {
4687
+ success: false,
4688
+ error,
4689
+ ...retry ? { retry } : {},
4690
+ billing: normalizeBilling(billing)
4691
+ }
4692
+ };
4693
+ }
4661
4694
  async function handleBatchOperationRoute(req, ctx) {
4662
4695
  const bodyResult = parseJsonBody(req);
4663
4696
  if (!bodyResult.success) {
@@ -4666,49 +4699,80 @@ async function handleBatchOperationRoute(req, ctx) {
4666
4699
  const body = bodyResult.data;
4667
4700
  const registry = ctx.batchOperationRegistry;
4668
4701
  if (!registry) {
4669
- return {
4670
- status: 404,
4671
- body: {
4672
- success: false,
4673
- error: "No batch operations registered"
4674
- }
4675
- };
4702
+ return softFailureResponse({
4703
+ code: "NOT_FOUND",
4704
+ message: "No batch operations registered",
4705
+ category: "validation"
4706
+ });
4676
4707
  }
4677
4708
  const operation = registry[body.handle];
4678
4709
  if (!operation) {
4679
- return {
4680
- status: 404,
4681
- body: {
4682
- success: false,
4683
- error: `Batch operation not found: ${body.handle}`
4684
- }
4685
- };
4710
+ return softFailureResponse({
4711
+ code: "NOT_FOUND",
4712
+ message: `Batch operation not found: ${body.handle}`,
4713
+ category: "validation"
4714
+ });
4686
4715
  }
4687
4716
  const context = body.context;
4688
4717
  if (!context?.workplaceId || !context?.appInstallationId || !context?.appId) {
4689
- return {
4690
- status: 400,
4691
- body: {
4692
- success: false,
4693
- error: "Missing context (workplaceId, appInstallationId, and appId required)"
4694
- }
4695
- };
4718
+ return softFailureResponse({
4719
+ code: "VALIDATION_ERROR",
4720
+ message: "Missing context (workplaceId, appInstallationId, and appId required)",
4721
+ category: "validation"
4722
+ });
4696
4723
  }
4697
- const env = body.env ?? {};
4698
- const log = {
4699
- info: (message, meta) => {
4700
- console.log(`[batch:${body.handle}] ${message}`, meta ?? "");
4701
- },
4702
- warn: (message, meta) => {
4703
- console.warn(`[batch:${body.handle}] ${message}`, meta ?? "");
4704
- },
4705
- error: (message, meta) => {
4706
- console.error(`[batch:${body.handle}] ${message}`, meta ?? "");
4707
- }
4724
+ if (body.method !== "setup" && body.method !== "iterate") {
4725
+ return softFailureResponse({
4726
+ code: "VALIDATION_ERROR",
4727
+ message: `Unknown method: ${body.method}`,
4728
+ category: "validation"
4729
+ });
4730
+ }
4731
+ const requestEnv = body.env ?? {};
4732
+ const mergedEnv = toStringEnv(buildToolExecutionEnv(requestEnv));
4733
+ const originalEnv = { ...process.env };
4734
+ Object.assign(process.env, mergedEnv);
4735
+ const log = createContextLogger();
4736
+ const app = context.app ?? {
4737
+ id: context.appId,
4738
+ versionId: context.appId
4739
+ };
4740
+ const workplace = context.workplace ?? {
4741
+ id: context.workplaceId,
4742
+ subdomain: ""
4743
+ };
4744
+ const invocation = body.invocation ?? {
4745
+ invocationId: `${body.handle}-${body.method}-${Date.now()}`,
4746
+ invocationType: "batch_operation",
4747
+ batchOperationHandle: body.handle,
4748
+ batchOperationMethod: body.method,
4749
+ appInstallationId: context.appInstallationId
4708
4750
  };
4709
4751
  const requestConfig = {
4710
- baseUrl: env.SKEDYUL_API_URL ?? process.env.SKEDYUL_API_URL ?? "",
4711
- apiToken: env.SKEDYUL_API_TOKEN ?? process.env.SKEDYUL_API_TOKEN ?? ""
4752
+ baseUrl: mergedEnv.SKEDYUL_API_URL ?? process.env.SKEDYUL_API_URL ?? "",
4753
+ apiToken: mergedEnv.SKEDYUL_API_TOKEN ?? process.env.SKEDYUL_API_TOKEN ?? ""
4754
+ };
4755
+ const rateLimitContext = {
4756
+ app,
4757
+ appInstallationId: context.appInstallationId,
4758
+ invocation,
4759
+ preAcquiredLeases: parsePreAcquiredLeases(
4760
+ mergedEnv.SKEDYUL_RATE_LIMIT_LEASES
4761
+ ),
4762
+ heldMutexQueueKeys: parseHeldMutexQueueKeys(
4763
+ mergedEnv.SKEDYUL_HELD_MUTEX_QUEUE_KEYS
4764
+ )
4765
+ };
4766
+ const opContextBase = {
4767
+ workplaceId: context.workplaceId,
4768
+ appInstallationId: context.appInstallationId,
4769
+ appId: context.appId,
4770
+ app,
4771
+ workplace,
4772
+ input: body.input,
4773
+ env: mergedEnv,
4774
+ invocation,
4775
+ log
4712
4776
  };
4713
4777
  try {
4714
4778
  if (body.method === "setup") {
@@ -4717,67 +4781,88 @@ async function handleBatchOperationRoute(req, ctx) {
4717
4781
  status: 200,
4718
4782
  body: {
4719
4783
  success: true,
4720
- result: { state: {}, total: void 0 }
4784
+ result: { state: {}, total: void 0 },
4785
+ billing: normalizeBilling(void 0)
4721
4786
  }
4722
4787
  };
4723
4788
  }
4724
- const result = await runWithConfig(requestConfig, async () => {
4725
- return operation.setup({
4726
- workplaceId: context.workplaceId,
4727
- appInstallationId: context.appInstallationId,
4728
- appId: context.appId,
4729
- input: body.input,
4730
- env,
4731
- log
4789
+ const raw2 = await runWithConfig(requestConfig, async () => {
4790
+ return runWithRateLimitExecutionContext(rateLimitContext, async () => {
4791
+ return runWithLogContext({ invocation }, async () => {
4792
+ return operation.setup(opContextBase);
4793
+ });
4732
4794
  });
4733
4795
  });
4796
+ if (isBatchOperationFailure(raw2)) {
4797
+ return softFailureResponse(raw2.error, raw2.retry, raw2.billing);
4798
+ }
4799
+ const setupResult = raw2;
4800
+ const domain2 = stripBillingFields(
4801
+ setupResult
4802
+ );
4734
4803
  return {
4735
4804
  status: 200,
4736
4805
  body: {
4737
4806
  success: true,
4738
- result
4807
+ result: domain2,
4808
+ billing: normalizeBilling(setupResult.billing)
4739
4809
  }
4740
4810
  };
4741
4811
  }
4742
- if (body.method === "iterate") {
4743
- const result = await runWithConfig(requestConfig, async () => {
4744
- return operation.iterate({
4745
- workplaceId: context.workplaceId,
4746
- appInstallationId: context.appInstallationId,
4747
- appId: context.appId,
4748
- input: body.input,
4749
- state: body.state,
4750
- page: body.page,
4751
- cursor: body.cursor,
4752
- limit: body.limit,
4753
- env,
4754
- log
4812
+ const raw = await runWithConfig(requestConfig, async () => {
4813
+ return runWithRateLimitExecutionContext(rateLimitContext, async () => {
4814
+ return runWithLogContext({ invocation }, async () => {
4815
+ return operation.iterate({
4816
+ ...opContextBase,
4817
+ state: body.state,
4818
+ page: body.page,
4819
+ cursor: body.cursor,
4820
+ limit: body.limit
4821
+ });
4755
4822
  });
4756
4823
  });
4757
- return {
4758
- status: 200,
4759
- body: {
4760
- success: true,
4761
- result
4762
- }
4763
- };
4824
+ });
4825
+ if (isBatchOperationFailure(raw)) {
4826
+ return softFailureResponse(raw.error, raw.retry, raw.billing);
4764
4827
  }
4828
+ const iterateResult = raw;
4829
+ const domain = stripBillingFields(
4830
+ iterateResult
4831
+ );
4765
4832
  return {
4766
- status: 400,
4833
+ status: 200,
4767
4834
  body: {
4768
- success: false,
4769
- error: `Unknown method: ${body.method}`
4835
+ success: true,
4836
+ result: domain,
4837
+ billing: normalizeBilling(iterateResult.billing)
4770
4838
  }
4771
4839
  };
4772
4840
  } catch (err) {
4773
- log.error("Batch operation failed", { error: err });
4841
+ if (err instanceof RateLimitExceededError) {
4842
+ return softFailureResponse(
4843
+ {
4844
+ code: "RATE_LIMITED",
4845
+ message: err.message,
4846
+ category: "external"
4847
+ },
4848
+ { allowed: true, afterMs: err.retryAfterMs }
4849
+ );
4850
+ }
4851
+ log.error("Batch operation failed", err);
4774
4852
  return {
4775
4853
  status: 500,
4776
4854
  body: {
4777
4855
  success: false,
4778
- error: err instanceof Error ? err.message : String(err)
4856
+ error: {
4857
+ code: "INTERNAL_ERROR",
4858
+ message: err instanceof Error ? err.message : String(err),
4859
+ category: "internal"
4860
+ },
4861
+ billing: normalizeBilling(void 0)
4779
4862
  }
4780
4863
  };
4864
+ } finally {
4865
+ process.env = originalEnv;
4781
4866
  }
4782
4867
  }
4783
4868
  function createNotFoundResponse() {
@@ -5,6 +5,10 @@
5
5
  * like member imports, data syncs, and batch API calls. The platform handles
6
6
  * orchestration, progress tracking, CRM mapping, and upsert.
7
7
  */
8
+ import type { InvocationContext } from '../../types/invocation';
9
+ import type { ToolBilling, ToolError, ToolRetry } from '../../types/tool';
10
+ import type { AppInfo, WorkplaceInfo } from '../../types/shared';
11
+ export type { ToolBilling, ToolError, ToolRetry };
8
12
  /**
9
13
  * Pagination response from an iterate function.
10
14
  * Supports both cursor-based and page-based pagination.
@@ -21,6 +25,15 @@ export interface BatchPagination {
21
25
  /** Items per page */
22
26
  limit?: number;
23
27
  }
28
+ /**
29
+ * Soft-failure return from setup/iterate (tool-call shaped).
30
+ */
31
+ export interface BatchOperationFailure {
32
+ success: false;
33
+ error: ToolError;
34
+ retry?: ToolRetry;
35
+ billing?: ToolBilling;
36
+ }
24
37
  /**
25
38
  * Result from a batch operation setup function.
26
39
  */
@@ -29,6 +42,10 @@ export interface BatchOperationSetupResult {
29
42
  state?: Record<string, unknown>;
30
43
  /** Optional total count for progress display */
31
44
  total?: number;
45
+ /** Optional billing (normalized by the route if omitted) */
46
+ billing?: ToolBilling;
47
+ /** Optional success discriminator (default true when domain fields present) */
48
+ success?: true;
32
49
  }
33
50
  /**
34
51
  * Result from a batch operation iterate function.
@@ -40,7 +57,13 @@ export interface BatchOperationIterateResult {
40
57
  pagination: BatchPagination;
41
58
  /** Optional state to persist for the next iterate call */
42
59
  state?: Record<string, unknown>;
60
+ /** Optional billing (normalized by the route if omitted) */
61
+ billing?: ToolBilling;
62
+ /** Optional success discriminator (default true when domain fields present) */
63
+ success?: true;
43
64
  }
65
+ export type BatchOperationSetupReturn = BatchOperationSetupResult | BatchOperationFailure;
66
+ export type BatchOperationIterateReturn = BatchOperationIterateResult | BatchOperationFailure;
44
67
  /**
45
68
  * Context passed to batch operation setup and iterate functions.
46
69
  */
@@ -51,15 +74,21 @@ export interface BatchOperationContext {
51
74
  appInstallationId: string;
52
75
  /** App ID */
53
76
  appId: string;
77
+ /** App info with version (for rate-limit / SDK context) */
78
+ app?: AppInfo;
79
+ /** Workplace info when available */
80
+ workplace?: WorkplaceInfo;
54
81
  /** User-provided input (if any) */
55
82
  input?: Record<string, unknown>;
56
83
  /** State from previous setup or iterate call */
57
84
  state?: Record<string, unknown>;
58
85
  /**
59
- * Runtime environment variables from the platform invoke
60
- * (APP_INSTALL overrides + SKEDYUL_API_TOKEN / SKEDYUL_API_URL).
86
+ * Merged environment (process.env + baked MCP_ENV + request env),
87
+ * same layering as tool handlers via buildToolExecutionEnv.
61
88
  */
62
89
  env: Record<string, string>;
90
+ /** Invocation context for log traceability */
91
+ invocation?: InvocationContext;
63
92
  /** Logger instance */
64
93
  log: {
65
94
  info: (message: string, meta?: Record<string, unknown>) => void;
@@ -70,7 +99,7 @@ export interface BatchOperationContext {
70
99
  /**
71
100
  * Type for batch operation setup function.
72
101
  */
73
- export type BatchOperationSetupFn = (ctx: BatchOperationContext) => Promise<BatchOperationSetupResult>;
102
+ export type BatchOperationSetupFn = (ctx: BatchOperationContext) => Promise<BatchOperationSetupReturn>;
74
103
  /**
75
104
  * Type for batch operation iterate function.
76
105
  */
@@ -81,7 +110,7 @@ export type BatchOperationIterateFn = (ctx: BatchOperationContext & {
81
110
  cursor?: string | number;
82
111
  /** Items per page */
83
112
  limit: number;
84
- }) => Promise<BatchOperationIterateResult>;
113
+ }) => Promise<BatchOperationIterateReturn>;
85
114
  /**
86
115
  * Batch operation definition.
87
116
  *
@@ -1988,6 +1988,39 @@ async function handleMcpBatchRoute(req, ctx) {
1988
1988
  }
1989
1989
  };
1990
1990
  }
1991
+ function isBatchOperationFailure(result) {
1992
+ return result != null && typeof result === "object" && result.success === false && typeof result.error === "object" && result.error != null;
1993
+ }
1994
+ function toStringEnv(env) {
1995
+ const out = {};
1996
+ for (const [key, value] of Object.entries(env)) {
1997
+ if (value !== void 0) {
1998
+ out[key] = value;
1999
+ }
2000
+ }
2001
+ return out;
2002
+ }
2003
+ function stripBillingFields(result) {
2004
+ const {
2005
+ billing: _billing,
2006
+ success: _success,
2007
+ error: _error,
2008
+ retry: _retry,
2009
+ ...domain
2010
+ } = result;
2011
+ return domain;
2012
+ }
2013
+ function softFailureResponse(error, retry, billing) {
2014
+ return {
2015
+ status: 200,
2016
+ body: {
2017
+ success: false,
2018
+ error,
2019
+ ...retry ? { retry } : {},
2020
+ billing: normalizeBilling(billing)
2021
+ }
2022
+ };
2023
+ }
1991
2024
  async function handleBatchOperationRoute(req, ctx) {
1992
2025
  const bodyResult = parseJsonBody(req);
1993
2026
  if (!bodyResult.success) {
@@ -1996,49 +2029,80 @@ async function handleBatchOperationRoute(req, ctx) {
1996
2029
  const body = bodyResult.data;
1997
2030
  const registry = ctx.batchOperationRegistry;
1998
2031
  if (!registry) {
1999
- return {
2000
- status: 404,
2001
- body: {
2002
- success: false,
2003
- error: "No batch operations registered"
2004
- }
2005
- };
2032
+ return softFailureResponse({
2033
+ code: "NOT_FOUND",
2034
+ message: "No batch operations registered",
2035
+ category: "validation"
2036
+ });
2006
2037
  }
2007
2038
  const operation = registry[body.handle];
2008
2039
  if (!operation) {
2009
- return {
2010
- status: 404,
2011
- body: {
2012
- success: false,
2013
- error: `Batch operation not found: ${body.handle}`
2014
- }
2015
- };
2040
+ return softFailureResponse({
2041
+ code: "NOT_FOUND",
2042
+ message: `Batch operation not found: ${body.handle}`,
2043
+ category: "validation"
2044
+ });
2016
2045
  }
2017
2046
  const context = body.context;
2018
2047
  if (!context?.workplaceId || !context?.appInstallationId || !context?.appId) {
2019
- return {
2020
- status: 400,
2021
- body: {
2022
- success: false,
2023
- error: "Missing context (workplaceId, appInstallationId, and appId required)"
2024
- }
2025
- };
2048
+ return softFailureResponse({
2049
+ code: "VALIDATION_ERROR",
2050
+ message: "Missing context (workplaceId, appInstallationId, and appId required)",
2051
+ category: "validation"
2052
+ });
2026
2053
  }
2027
- const env = body.env ?? {};
2028
- const log = {
2029
- info: (message, meta) => {
2030
- console.log(`[batch:${body.handle}] ${message}`, meta ?? "");
2031
- },
2032
- warn: (message, meta) => {
2033
- console.warn(`[batch:${body.handle}] ${message}`, meta ?? "");
2034
- },
2035
- error: (message, meta) => {
2036
- console.error(`[batch:${body.handle}] ${message}`, meta ?? "");
2037
- }
2054
+ if (body.method !== "setup" && body.method !== "iterate") {
2055
+ return softFailureResponse({
2056
+ code: "VALIDATION_ERROR",
2057
+ message: `Unknown method: ${body.method}`,
2058
+ category: "validation"
2059
+ });
2060
+ }
2061
+ const requestEnv = body.env ?? {};
2062
+ const mergedEnv = toStringEnv(buildToolExecutionEnv(requestEnv));
2063
+ const originalEnv = { ...process.env };
2064
+ Object.assign(process.env, mergedEnv);
2065
+ const log = createContextLogger();
2066
+ const app = context.app ?? {
2067
+ id: context.appId,
2068
+ versionId: context.appId
2069
+ };
2070
+ const workplace = context.workplace ?? {
2071
+ id: context.workplaceId,
2072
+ subdomain: ""
2073
+ };
2074
+ const invocation = body.invocation ?? {
2075
+ invocationId: `${body.handle}-${body.method}-${Date.now()}`,
2076
+ invocationType: "batch_operation",
2077
+ batchOperationHandle: body.handle,
2078
+ batchOperationMethod: body.method,
2079
+ appInstallationId: context.appInstallationId
2038
2080
  };
2039
2081
  const requestConfig = {
2040
- baseUrl: env.SKEDYUL_API_URL ?? process.env.SKEDYUL_API_URL ?? "",
2041
- apiToken: env.SKEDYUL_API_TOKEN ?? process.env.SKEDYUL_API_TOKEN ?? ""
2082
+ baseUrl: mergedEnv.SKEDYUL_API_URL ?? process.env.SKEDYUL_API_URL ?? "",
2083
+ apiToken: mergedEnv.SKEDYUL_API_TOKEN ?? process.env.SKEDYUL_API_TOKEN ?? ""
2084
+ };
2085
+ const rateLimitContext = {
2086
+ app,
2087
+ appInstallationId: context.appInstallationId,
2088
+ invocation,
2089
+ preAcquiredLeases: parsePreAcquiredLeases(
2090
+ mergedEnv.SKEDYUL_RATE_LIMIT_LEASES
2091
+ ),
2092
+ heldMutexQueueKeys: parseHeldMutexQueueKeys(
2093
+ mergedEnv.SKEDYUL_HELD_MUTEX_QUEUE_KEYS
2094
+ )
2095
+ };
2096
+ const opContextBase = {
2097
+ workplaceId: context.workplaceId,
2098
+ appInstallationId: context.appInstallationId,
2099
+ appId: context.appId,
2100
+ app,
2101
+ workplace,
2102
+ input: body.input,
2103
+ env: mergedEnv,
2104
+ invocation,
2105
+ log
2042
2106
  };
2043
2107
  try {
2044
2108
  if (body.method === "setup") {
@@ -2047,67 +2111,88 @@ async function handleBatchOperationRoute(req, ctx) {
2047
2111
  status: 200,
2048
2112
  body: {
2049
2113
  success: true,
2050
- result: { state: {}, total: void 0 }
2114
+ result: { state: {}, total: void 0 },
2115
+ billing: normalizeBilling(void 0)
2051
2116
  }
2052
2117
  };
2053
2118
  }
2054
- const result = await runWithConfig(requestConfig, async () => {
2055
- return operation.setup({
2056
- workplaceId: context.workplaceId,
2057
- appInstallationId: context.appInstallationId,
2058
- appId: context.appId,
2059
- input: body.input,
2060
- env,
2061
- log
2119
+ const raw2 = await runWithConfig(requestConfig, async () => {
2120
+ return runWithRateLimitExecutionContext(rateLimitContext, async () => {
2121
+ return runWithLogContext({ invocation }, async () => {
2122
+ return operation.setup(opContextBase);
2123
+ });
2062
2124
  });
2063
2125
  });
2126
+ if (isBatchOperationFailure(raw2)) {
2127
+ return softFailureResponse(raw2.error, raw2.retry, raw2.billing);
2128
+ }
2129
+ const setupResult = raw2;
2130
+ const domain2 = stripBillingFields(
2131
+ setupResult
2132
+ );
2064
2133
  return {
2065
2134
  status: 200,
2066
2135
  body: {
2067
2136
  success: true,
2068
- result
2137
+ result: domain2,
2138
+ billing: normalizeBilling(setupResult.billing)
2069
2139
  }
2070
2140
  };
2071
2141
  }
2072
- if (body.method === "iterate") {
2073
- const result = await runWithConfig(requestConfig, async () => {
2074
- return operation.iterate({
2075
- workplaceId: context.workplaceId,
2076
- appInstallationId: context.appInstallationId,
2077
- appId: context.appId,
2078
- input: body.input,
2079
- state: body.state,
2080
- page: body.page,
2081
- cursor: body.cursor,
2082
- limit: body.limit,
2083
- env,
2084
- log
2142
+ const raw = await runWithConfig(requestConfig, async () => {
2143
+ return runWithRateLimitExecutionContext(rateLimitContext, async () => {
2144
+ return runWithLogContext({ invocation }, async () => {
2145
+ return operation.iterate({
2146
+ ...opContextBase,
2147
+ state: body.state,
2148
+ page: body.page,
2149
+ cursor: body.cursor,
2150
+ limit: body.limit
2151
+ });
2085
2152
  });
2086
2153
  });
2087
- return {
2088
- status: 200,
2089
- body: {
2090
- success: true,
2091
- result
2092
- }
2093
- };
2154
+ });
2155
+ if (isBatchOperationFailure(raw)) {
2156
+ return softFailureResponse(raw.error, raw.retry, raw.billing);
2094
2157
  }
2158
+ const iterateResult = raw;
2159
+ const domain = stripBillingFields(
2160
+ iterateResult
2161
+ );
2095
2162
  return {
2096
- status: 400,
2163
+ status: 200,
2097
2164
  body: {
2098
- success: false,
2099
- error: `Unknown method: ${body.method}`
2165
+ success: true,
2166
+ result: domain,
2167
+ billing: normalizeBilling(iterateResult.billing)
2100
2168
  }
2101
2169
  };
2102
2170
  } catch (err) {
2103
- log.error("Batch operation failed", { error: err });
2171
+ if (err instanceof RateLimitExceededError) {
2172
+ return softFailureResponse(
2173
+ {
2174
+ code: "RATE_LIMITED",
2175
+ message: err.message,
2176
+ category: "external"
2177
+ },
2178
+ { allowed: true, afterMs: err.retryAfterMs }
2179
+ );
2180
+ }
2181
+ log.error("Batch operation failed", err);
2104
2182
  return {
2105
2183
  status: 500,
2106
2184
  body: {
2107
2185
  success: false,
2108
- error: err instanceof Error ? err.message : String(err)
2186
+ error: {
2187
+ code: "INTERNAL_ERROR",
2188
+ message: err instanceof Error ? err.message : String(err),
2189
+ category: "internal"
2190
+ },
2191
+ billing: normalizeBilling(void 0)
2109
2192
  }
2110
2193
  };
2194
+ } finally {
2195
+ process.env = originalEnv;
2111
2196
  }
2112
2197
  }
2113
2198
  function createNotFoundResponse() {