@tradejs/node 3.1.29-beta.272 → 3.1.29-beta.275

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/mcp.d.mts CHANGED
@@ -8,10 +8,11 @@ declare const mcpPackageManifestHash: () => Promise<string>;
8
8
  declare const enqueueMcpJob: (principal: McpPrincipal, kind: McpJob["kind"], request: Record<string, unknown>, idempotencyKey: string) => Promise<McpJob>;
9
9
  declare const readMcpJob: (userName: string, id: string) => Promise<McpJob>;
10
10
  declare const readAuthorizedMcpJob: (principal: McpPrincipal, id: string) => Promise<McpJob>;
11
+ declare const redactMcpJobLog: (line: string) => string;
11
12
  /** Offsets refer to the retained worker tail, which may shift while running. */
12
13
  declare const readMcpJobLogs: (principal: McpPrincipal, id: string, offset?: number, limit?: number) => Promise<{
13
14
  jobId: string;
14
- status: "completed" | "failed" | "running" | "queued" | "cancelled";
15
+ status: "running" | "completed" | "failed" | "queued" | "cancelled";
15
16
  updatedAt: number;
16
17
  offset: number;
17
18
  nextOffset: number | null;
@@ -144,6 +145,10 @@ declare const readMcpStrategies: (userName: string, deploymentId: string) => Pro
144
145
  }[];
145
146
  observedAt: number;
146
147
  }>;
148
+ declare class McpCursorError extends Error {
149
+ readonly code: 'CURSOR_EXPIRED' | 'CURSOR_MISMATCH';
150
+ constructor(code: 'CURSOR_EXPIRED' | 'CURSOR_MISMATCH');
151
+ }
147
152
  declare const readMcpRuntimePage: (userName: string, kind: "signals" | "evaluations" | "orders", request: {
148
153
  deploymentId: string;
149
154
  cursor: string;
@@ -151,6 +156,7 @@ declare const readMcpRuntimePage: (userName: string, kind: "signals" | "evaluati
151
156
  endTime: number;
152
157
  strategy?: string;
153
158
  symbol?: string;
159
+ limit?: number;
154
160
  }) => Promise<{
155
161
  items: Record<string, unknown>[];
156
162
  cursor: string;
@@ -162,6 +168,16 @@ declare const readMcpRuntimePage: (userName: string, kind: "signals" | "evaluati
162
168
  endTime: number;
163
169
  };
164
170
  coverage: string;
171
+ pagination: {
172
+ scanComplete: boolean;
173
+ storageReads: number;
174
+ inspected: number;
175
+ cursorTtlSeconds: number;
176
+ consistency: string;
177
+ order: string;
178
+ retentionStartTime: null;
179
+ persistenceCompleteness: string;
180
+ };
165
181
  note: string;
166
182
  }>;
167
183
  declare const readMcpSignal: (userName: string, signalId: string, deploymentId: string, timestamp: number, strategy: string) => Promise<{
@@ -212,4 +228,4 @@ declare const readMcpBacktestResult: (userName: string, id: string, offset?: num
212
228
  source: string;
213
229
  }>;
214
230
 
215
- export { MCP_JOB_TTL, cancelMcpJob, enqueueMcpJob, isMcpRecord, mcpId, mcpJobDirectory, mcpJobStorageId, mcpPackageManifestHash, mcpProjectRoot, publicMcpJob, readAuthorizedMcpJob, readMcpBacktestPage, readMcpBacktestResult, readMcpChart, readMcpDeployments, readMcpEvaluationStats, readMcpJob, readMcpJobLogs, readMcpMarket, readMcpRuntimePage, readMcpSignal, readMcpStrategies, redactMcpData, requireMcpDeployment };
231
+ export { MCP_JOB_TTL, McpCursorError, cancelMcpJob, enqueueMcpJob, isMcpRecord, mcpId, mcpJobDirectory, mcpJobStorageId, mcpPackageManifestHash, mcpProjectRoot, publicMcpJob, readAuthorizedMcpJob, readMcpBacktestPage, readMcpBacktestResult, readMcpChart, readMcpDeployments, readMcpEvaluationStats, readMcpJob, readMcpJobLogs, readMcpMarket, readMcpRuntimePage, readMcpSignal, readMcpStrategies, redactMcpData, redactMcpJobLog, requireMcpDeployment };
package/dist/mcp.d.ts CHANGED
@@ -8,10 +8,11 @@ declare const mcpPackageManifestHash: () => Promise<string>;
8
8
  declare const enqueueMcpJob: (principal: McpPrincipal, kind: McpJob["kind"], request: Record<string, unknown>, idempotencyKey: string) => Promise<McpJob>;
9
9
  declare const readMcpJob: (userName: string, id: string) => Promise<McpJob>;
10
10
  declare const readAuthorizedMcpJob: (principal: McpPrincipal, id: string) => Promise<McpJob>;
11
+ declare const redactMcpJobLog: (line: string) => string;
11
12
  /** Offsets refer to the retained worker tail, which may shift while running. */
12
13
  declare const readMcpJobLogs: (principal: McpPrincipal, id: string, offset?: number, limit?: number) => Promise<{
13
14
  jobId: string;
14
- status: "completed" | "failed" | "running" | "queued" | "cancelled";
15
+ status: "running" | "completed" | "failed" | "queued" | "cancelled";
15
16
  updatedAt: number;
16
17
  offset: number;
17
18
  nextOffset: number | null;
@@ -144,6 +145,10 @@ declare const readMcpStrategies: (userName: string, deploymentId: string) => Pro
144
145
  }[];
145
146
  observedAt: number;
146
147
  }>;
148
+ declare class McpCursorError extends Error {
149
+ readonly code: 'CURSOR_EXPIRED' | 'CURSOR_MISMATCH';
150
+ constructor(code: 'CURSOR_EXPIRED' | 'CURSOR_MISMATCH');
151
+ }
147
152
  declare const readMcpRuntimePage: (userName: string, kind: "signals" | "evaluations" | "orders", request: {
148
153
  deploymentId: string;
149
154
  cursor: string;
@@ -151,6 +156,7 @@ declare const readMcpRuntimePage: (userName: string, kind: "signals" | "evaluati
151
156
  endTime: number;
152
157
  strategy?: string;
153
158
  symbol?: string;
159
+ limit?: number;
154
160
  }) => Promise<{
155
161
  items: Record<string, unknown>[];
156
162
  cursor: string;
@@ -162,6 +168,16 @@ declare const readMcpRuntimePage: (userName: string, kind: "signals" | "evaluati
162
168
  endTime: number;
163
169
  };
164
170
  coverage: string;
171
+ pagination: {
172
+ scanComplete: boolean;
173
+ storageReads: number;
174
+ inspected: number;
175
+ cursorTtlSeconds: number;
176
+ consistency: string;
177
+ order: string;
178
+ retentionStartTime: null;
179
+ persistenceCompleteness: string;
180
+ };
165
181
  note: string;
166
182
  }>;
167
183
  declare const readMcpSignal: (userName: string, signalId: string, deploymentId: string, timestamp: number, strategy: string) => Promise<{
@@ -212,4 +228,4 @@ declare const readMcpBacktestResult: (userName: string, id: string, offset?: num
212
228
  source: string;
213
229
  }>;
214
230
 
215
- export { MCP_JOB_TTL, cancelMcpJob, enqueueMcpJob, isMcpRecord, mcpId, mcpJobDirectory, mcpJobStorageId, mcpPackageManifestHash, mcpProjectRoot, publicMcpJob, readAuthorizedMcpJob, readMcpBacktestPage, readMcpBacktestResult, readMcpChart, readMcpDeployments, readMcpEvaluationStats, readMcpJob, readMcpJobLogs, readMcpMarket, readMcpRuntimePage, readMcpSignal, readMcpStrategies, redactMcpData, requireMcpDeployment };
231
+ export { MCP_JOB_TTL, McpCursorError, cancelMcpJob, enqueueMcpJob, isMcpRecord, mcpId, mcpJobDirectory, mcpJobStorageId, mcpPackageManifestHash, mcpProjectRoot, publicMcpJob, readAuthorizedMcpJob, readMcpBacktestPage, readMcpBacktestResult, readMcpChart, readMcpDeployments, readMcpEvaluationStats, readMcpJob, readMcpJobLogs, readMcpMarket, readMcpRuntimePage, readMcpSignal, readMcpStrategies, redactMcpData, redactMcpJobLog, requireMcpDeployment };
package/dist/mcp.js CHANGED
@@ -778,6 +778,7 @@ var init_jevTraining = __esm({
778
778
  var mcp_exports = {};
779
779
  __export(mcp_exports, {
780
780
  MCP_JOB_TTL: () => MCP_JOB_TTL,
781
+ McpCursorError: () => McpCursorError,
781
782
  cancelMcpJob: () => cancelMcpJob,
782
783
  enqueueMcpJob: () => enqueueMcpJob,
783
784
  isMcpRecord: () => isMcpRecord,
@@ -800,6 +801,7 @@ __export(mcp_exports, {
800
801
  readMcpSignal: () => readMcpSignal,
801
802
  readMcpStrategies: () => readMcpStrategies,
802
803
  redactMcpData: () => redactMcpData,
804
+ redactMcpJobLog: () => redactMcpJobLog,
803
805
  requireMcpDeployment: () => requireMcpDeployment
804
806
  });
805
807
  module.exports = __toCommonJS(mcp_exports);
@@ -8874,7 +8876,7 @@ var readAuthorizedMcpJob = async (principal, id) => {
8874
8876
  throw new Error("Insufficient scope");
8875
8877
  return job;
8876
8878
  };
8877
- var redactJobLog = (line) => line.replace(/\u001b\[[0-9;]*m/g, "").replace(/\b(Bearer|Basic)\s+\S+/gi, "$1 [redacted]").replace(/([a-z][a-z0-9+.-]*:\/\/)[^\s/@]*:[^\s/@]+@/gi, "$1[redacted]@").replace(/((?:authorization|cookie)\s*:\s*)[^\r\n]+/gi, "$1[redacted]").replace(
8879
+ var redactMcpJobLog = (line) => line.replace(/\u001b\[[0-9;]*m/g, "").replace(/\b(Bearer|Basic)\s+\S+/gi, "$1 [redacted]").replace(/([a-z][a-z0-9+.-]*:\/\/)[^\s/@]*:[^\s/@]+@/gi, "$1[redacted]@").replace(/((?:authorization|cookie)\s*:\s*)[^\r\n]+/gi, "$1[redacted]").replace(
8878
8880
  /((?:[\w.-]*(?:secret|password|token|api[_-]?key|private[_-]?key|authorization|cookie|credential)[\w.-]*)["']?\s*[:=]\s*)(?:"[^"\r\n]*"|'[^'\r\n]*'|[^\s,;&}]+)/gi,
8879
8881
  "$1[redacted]"
8880
8882
  ).slice(0, 1e3);
@@ -8885,7 +8887,7 @@ var readMcpJobLogs = async (principal, id, offset = 0, limit = 50) => {
8885
8887
  const lines = [];
8886
8888
  let bytes = 0;
8887
8889
  for (const line of job.logs.slice(offset, offset + limit)) {
8888
- const redacted = redactJobLog(line);
8890
+ const redacted = redactMcpJobLog(line);
8889
8891
  const size = Buffer.byteLength(JSON.stringify(redacted)) + 1;
8890
8892
  if (bytes + size > 48e3) break;
8891
8893
  lines.push(redacted);
@@ -9081,11 +9083,34 @@ var readMcpStrategies = async (userName, deploymentId) => {
9081
9083
  observedAt: Date.now()
9082
9084
  };
9083
9085
  };
9086
+ var McpCursorError = class extends Error {
9087
+ constructor(code) {
9088
+ super(
9089
+ code === "CURSOR_EXPIRED" ? "Invalid storage cursor: expired or unavailable; restart the same window and deduplicate record identities" : "Invalid storage cursor: user, account or filters changed"
9090
+ );
9091
+ this.code = code;
9092
+ this.name = "McpCursorError";
9093
+ }
9094
+ };
9095
+ var RUNTIME_CURSOR_TTL = 86400;
9084
9096
  var readMcpRuntimePage = async (userName, kind, request) => {
9085
9097
  const deployment = await requireMcpDeployment(userName, request.deploymentId);
9086
9098
  const prefix = kind === "signals" ? import_redis4.redisKeys.runtimeSignalBuckets(userName) : kind === "evaluations" ? import_redis4.redisKeys.runtimeSignalEvaluationBuckets(userName) : import_redis4.redisKeys.runtimeTrades(userName);
9087
- const { cursor: _cursor, ...filters } = request;
9088
- const binding = mcpId(JSON.stringify({ userName, kind, ...filters }));
9099
+ const limit = request.limit ?? 50;
9100
+ if (!Number.isInteger(limit) || limit < 1 || limit > 50)
9101
+ throw new Error("Select a page limit of 1\u201350");
9102
+ const binding = mcpId(
9103
+ JSON.stringify({
9104
+ userName,
9105
+ kind,
9106
+ accountId: deployment.accountId,
9107
+ deploymentId: request.deploymentId,
9108
+ startTime: request.startTime,
9109
+ endTime: request.endTime,
9110
+ strategy: request.strategy ?? null,
9111
+ symbol: request.symbol ?? null
9112
+ })
9113
+ );
9089
9114
  let state = {
9090
9115
  binding,
9091
9116
  scan: "0",
@@ -9098,29 +9123,46 @@ var readMcpRuntimePage = async (userName, kind, request) => {
9098
9123
  const saved = await import_redis4.mcpStorage.get(
9099
9124
  `cursor:${request.cursor}`
9100
9125
  );
9101
- if (!saved || saved.binding !== binding)
9102
- throw new Error("Invalid storage cursor");
9103
- state = saved;
9104
- }
9105
- if (!state.keys.length && !state.values.length && (!state.scanned || state.scan !== "0")) {
9106
- const page = await (0, import_redis4.scanDataKeys)(prefix, state.scan);
9107
- state = { ...state, scan: page.cursor, keys: page.keys, scanned: true };
9108
- }
9109
- if (!state.values.length && state.keys.length) {
9110
- const key = state.keys[0];
9111
- if (kind === "orders") {
9112
- state.keys.shift();
9113
- if (!key.slice(prefix.length).startsWith("days:"))
9114
- state.values = [await (0, import_redis4.getDataStrict)(key)];
9115
- } else {
9116
- const page = await (0, import_redis4.scanHashJson)(key, state.hash);
9117
- state.values = page.values;
9118
- state.hash = page.cursor;
9119
- if (page.cursor === "0") state.keys.shift();
9120
- }
9126
+ if (!saved) throw new McpCursorError("CURSOR_EXPIRED");
9127
+ if (saved.binding !== binding) throw new McpCursorError("CURSOR_MISMATCH");
9128
+ state = structuredClone(saved);
9121
9129
  }
9122
9130
  const items = [];
9123
- for (let row of state.values.splice(0, 50)) {
9131
+ let storageReads = 0;
9132
+ let inspected = 0;
9133
+ let responseBytes = 0;
9134
+ while (items.length < limit && storageReads < 40 && inspected < 1e3) {
9135
+ if (!state.values.length) {
9136
+ if (!state.keys.length) {
9137
+ if (state.scanned && state.scan === "0") break;
9138
+ const page = await (0, import_redis4.scanDataKeys)(prefix, state.scan);
9139
+ storageReads++;
9140
+ state.scan = page.cursor;
9141
+ state.scanned = true;
9142
+ state.keys = page.keys.filter((key2) => {
9143
+ if (kind === "orders")
9144
+ return !key2.slice(prefix.length).startsWith("days:");
9145
+ const suffix = key2.slice(prefix.length);
9146
+ const day = suffix.slice(0, 10);
9147
+ return day >= (0, import_time3.getRuntimeStorageDayKey)(request.startTime) && day <= (0, import_time3.getRuntimeStorageDayKey)(request.endTime - 1) && (!request.strategy || suffix.slice(11) === request.strategy);
9148
+ });
9149
+ if (!state.keys.length) continue;
9150
+ }
9151
+ const key = state.keys[0];
9152
+ storageReads++;
9153
+ if (kind === "orders") {
9154
+ state.keys.shift();
9155
+ state.values = [await (0, import_redis4.getDataStrict)(key)];
9156
+ } else {
9157
+ const page = await (0, import_redis4.scanHashJson)(key, state.hash);
9158
+ state.values = page.values;
9159
+ state.hash = page.cursor;
9160
+ if (page.cursor === "0") state.keys.shift();
9161
+ }
9162
+ if (!state.values.length) continue;
9163
+ }
9164
+ let row = state.values.shift();
9165
+ inspected++;
9124
9166
  if (kind === "signals" && isMcpRecord(row) && typeof row.symbol === "string" && typeof row.signalId === "string")
9125
9167
  row = await (0, import_redis4.getDataStrict)(
9126
9168
  import_redis4.redisKeys.storeSignal(row.symbol, row.signalId)
@@ -9138,12 +9180,23 @@ var readMcpRuntimePage = async (userName, kind, request) => {
9138
9180
  additionalIndicators: _additional,
9139
9181
  ...summary
9140
9182
  } = row;
9183
+ const bytes = Buffer.byteLength(JSON.stringify(summary));
9184
+ if (bytes > 8e4)
9185
+ throw new Error(
9186
+ "Response too large. Use runtime_get_signal for record details."
9187
+ );
9188
+ if (responseBytes + bytes > 8e4) {
9189
+ state.values.unshift(row);
9190
+ break;
9191
+ }
9192
+ responseBytes += bytes;
9141
9193
  items.push(summary);
9142
9194
  }
9143
9195
  let next = "0";
9144
9196
  if (state.values.length || state.keys.length || state.scan !== "0") {
9145
- next = mcpId((0, import_node_crypto6.randomUUID)());
9146
- await import_redis4.mcpStorage.put(`cursor:${next}`, state, 600);
9197
+ next = mcpId(JSON.stringify(state));
9198
+ if (!await import_redis4.mcpStorage.put(`cursor:${next}`, state, RUNTIME_CURSOR_TTL))
9199
+ throw new Error("MCP cursor persistence failed");
9147
9200
  }
9148
9201
  return {
9149
9202
  items,
@@ -9153,7 +9206,17 @@ var readMcpRuntimePage = async (userName, kind, request) => {
9153
9206
  source: "runtime_storage",
9154
9207
  window: { startTime: request.startTime, endTime: request.endTime },
9155
9208
  coverage: "stored_records_only",
9156
- note: "Missing records do not prove no evaluation occurred. Continue until cursor is 0; cursors expire after 10 minutes."
9209
+ pagination: {
9210
+ scanComplete: next === "0",
9211
+ storageReads,
9212
+ inspected,
9213
+ cursorTtlSeconds: RUNTIME_CURSOR_TTL,
9214
+ consistency: "mutable_storage_scan",
9215
+ order: "storage_scan",
9216
+ retentionStartTime: null,
9217
+ persistenceCompleteness: "unknown"
9218
+ },
9219
+ note: "Continue until cursor=0, including empty pages. Cursors are shared across server processes and retained for 24 hours. Retry identical requests after transport failures. If CURSOR_EXPIRED, restart the same window and deduplicate record identities. Storage scans may repeat rows and are not immutable snapshots; missing records do not prove no activity."
9157
9220
  };
9158
9221
  };
9159
9222
  var readMcpSignal = async (userName, signalId, deploymentId, timestamp, strategy) => {
@@ -9273,6 +9336,7 @@ var readMcpBacktestResult = async (userName, id, offset = 0, limit = 20) => {
9273
9336
  // Annotate the CommonJS export names for ESM import in node:
9274
9337
  0 && (module.exports = {
9275
9338
  MCP_JOB_TTL,
9339
+ McpCursorError,
9276
9340
  cancelMcpJob,
9277
9341
  enqueueMcpJob,
9278
9342
  isMcpRecord,
@@ -9295,5 +9359,6 @@ var readMcpBacktestResult = async (userName, id, offset = 0, limit = 20) => {
9295
9359
  readMcpSignal,
9296
9360
  readMcpStrategies,
9297
9361
  redactMcpData,
9362
+ redactMcpJobLog,
9298
9363
  requireMcpDeployment
9299
9364
  });
package/dist/mcp.mjs CHANGED
@@ -17,7 +17,7 @@ import "./chunk-GHSLOJLE.mjs";
17
17
  import "./chunk-Y6FXYEAI.mjs";
18
18
 
19
19
  // src/mcp.ts
20
- import { createHash as createHash2, randomUUID } from "crypto";
20
+ import { createHash as createHash2 } from "crypto";
21
21
  import { createIndicators } from "@tradejs/core/indicators";
22
22
  import { intervalToMs } from "@tradejs/core/data";
23
23
  import {
@@ -200,7 +200,7 @@ var readAuthorizedMcpJob = async (principal, id) => {
200
200
  throw new Error("Insufficient scope");
201
201
  return job;
202
202
  };
203
- var redactJobLog = (line) => line.replace(/\u001b\[[0-9;]*m/g, "").replace(/\b(Bearer|Basic)\s+\S+/gi, "$1 [redacted]").replace(/([a-z][a-z0-9+.-]*:\/\/)[^\s/@]*:[^\s/@]+@/gi, "$1[redacted]@").replace(/((?:authorization|cookie)\s*:\s*)[^\r\n]+/gi, "$1[redacted]").replace(
203
+ var redactMcpJobLog = (line) => line.replace(/\u001b\[[0-9;]*m/g, "").replace(/\b(Bearer|Basic)\s+\S+/gi, "$1 [redacted]").replace(/([a-z][a-z0-9+.-]*:\/\/)[^\s/@]*:[^\s/@]+@/gi, "$1[redacted]@").replace(/((?:authorization|cookie)\s*:\s*)[^\r\n]+/gi, "$1[redacted]").replace(
204
204
  /((?:[\w.-]*(?:secret|password|token|api[_-]?key|private[_-]?key|authorization|cookie|credential)[\w.-]*)["']?\s*[:=]\s*)(?:"[^"\r\n]*"|'[^'\r\n]*'|[^\s,;&}]+)/gi,
205
205
  "$1[redacted]"
206
206
  ).slice(0, 1e3);
@@ -211,7 +211,7 @@ var readMcpJobLogs = async (principal, id, offset = 0, limit = 50) => {
211
211
  const lines = [];
212
212
  let bytes = 0;
213
213
  for (const line of job.logs.slice(offset, offset + limit)) {
214
- const redacted = redactJobLog(line);
214
+ const redacted = redactMcpJobLog(line);
215
215
  const size = Buffer.byteLength(JSON.stringify(redacted)) + 1;
216
216
  if (bytes + size > 48e3) break;
217
217
  lines.push(redacted);
@@ -407,11 +407,34 @@ var readMcpStrategies = async (userName, deploymentId) => {
407
407
  observedAt: Date.now()
408
408
  };
409
409
  };
410
+ var McpCursorError = class extends Error {
411
+ constructor(code) {
412
+ super(
413
+ code === "CURSOR_EXPIRED" ? "Invalid storage cursor: expired or unavailable; restart the same window and deduplicate record identities" : "Invalid storage cursor: user, account or filters changed"
414
+ );
415
+ this.code = code;
416
+ this.name = "McpCursorError";
417
+ }
418
+ };
419
+ var RUNTIME_CURSOR_TTL = 86400;
410
420
  var readMcpRuntimePage = async (userName, kind, request) => {
411
421
  const deployment = await requireMcpDeployment(userName, request.deploymentId);
412
422
  const prefix = kind === "signals" ? redisKeys2.runtimeSignalBuckets(userName) : kind === "evaluations" ? redisKeys2.runtimeSignalEvaluationBuckets(userName) : redisKeys2.runtimeTrades(userName);
413
- const { cursor: _cursor, ...filters } = request;
414
- const binding = mcpId(JSON.stringify({ userName, kind, ...filters }));
423
+ const limit = request.limit ?? 50;
424
+ if (!Number.isInteger(limit) || limit < 1 || limit > 50)
425
+ throw new Error("Select a page limit of 1\u201350");
426
+ const binding = mcpId(
427
+ JSON.stringify({
428
+ userName,
429
+ kind,
430
+ accountId: deployment.accountId,
431
+ deploymentId: request.deploymentId,
432
+ startTime: request.startTime,
433
+ endTime: request.endTime,
434
+ strategy: request.strategy ?? null,
435
+ symbol: request.symbol ?? null
436
+ })
437
+ );
415
438
  let state = {
416
439
  binding,
417
440
  scan: "0",
@@ -424,29 +447,46 @@ var readMcpRuntimePage = async (userName, kind, request) => {
424
447
  const saved = await mcpStorage2.get(
425
448
  `cursor:${request.cursor}`
426
449
  );
427
- if (!saved || saved.binding !== binding)
428
- throw new Error("Invalid storage cursor");
429
- state = saved;
430
- }
431
- if (!state.keys.length && !state.values.length && (!state.scanned || state.scan !== "0")) {
432
- const page = await scanDataKeys(prefix, state.scan);
433
- state = { ...state, scan: page.cursor, keys: page.keys, scanned: true };
450
+ if (!saved) throw new McpCursorError("CURSOR_EXPIRED");
451
+ if (saved.binding !== binding) throw new McpCursorError("CURSOR_MISMATCH");
452
+ state = structuredClone(saved);
434
453
  }
435
- if (!state.values.length && state.keys.length) {
436
- const key = state.keys[0];
437
- if (kind === "orders") {
438
- state.keys.shift();
439
- if (!key.slice(prefix.length).startsWith("days:"))
454
+ const items = [];
455
+ let storageReads = 0;
456
+ let inspected = 0;
457
+ let responseBytes = 0;
458
+ while (items.length < limit && storageReads < 40 && inspected < 1e3) {
459
+ if (!state.values.length) {
460
+ if (!state.keys.length) {
461
+ if (state.scanned && state.scan === "0") break;
462
+ const page = await scanDataKeys(prefix, state.scan);
463
+ storageReads++;
464
+ state.scan = page.cursor;
465
+ state.scanned = true;
466
+ state.keys = page.keys.filter((key2) => {
467
+ if (kind === "orders")
468
+ return !key2.slice(prefix.length).startsWith("days:");
469
+ const suffix = key2.slice(prefix.length);
470
+ const day = suffix.slice(0, 10);
471
+ return day >= getRuntimeStorageDayKey(request.startTime) && day <= getRuntimeStorageDayKey(request.endTime - 1) && (!request.strategy || suffix.slice(11) === request.strategy);
472
+ });
473
+ if (!state.keys.length) continue;
474
+ }
475
+ const key = state.keys[0];
476
+ storageReads++;
477
+ if (kind === "orders") {
478
+ state.keys.shift();
440
479
  state.values = [await getDataStrict2(key)];
441
- } else {
442
- const page = await scanHashJson(key, state.hash);
443
- state.values = page.values;
444
- state.hash = page.cursor;
445
- if (page.cursor === "0") state.keys.shift();
480
+ } else {
481
+ const page = await scanHashJson(key, state.hash);
482
+ state.values = page.values;
483
+ state.hash = page.cursor;
484
+ if (page.cursor === "0") state.keys.shift();
485
+ }
486
+ if (!state.values.length) continue;
446
487
  }
447
- }
448
- const items = [];
449
- for (let row of state.values.splice(0, 50)) {
488
+ let row = state.values.shift();
489
+ inspected++;
450
490
  if (kind === "signals" && isMcpRecord(row) && typeof row.symbol === "string" && typeof row.signalId === "string")
451
491
  row = await getDataStrict2(
452
492
  redisKeys2.storeSignal(row.symbol, row.signalId)
@@ -464,12 +504,23 @@ var readMcpRuntimePage = async (userName, kind, request) => {
464
504
  additionalIndicators: _additional,
465
505
  ...summary
466
506
  } = row;
507
+ const bytes = Buffer.byteLength(JSON.stringify(summary));
508
+ if (bytes > 8e4)
509
+ throw new Error(
510
+ "Response too large. Use runtime_get_signal for record details."
511
+ );
512
+ if (responseBytes + bytes > 8e4) {
513
+ state.values.unshift(row);
514
+ break;
515
+ }
516
+ responseBytes += bytes;
467
517
  items.push(summary);
468
518
  }
469
519
  let next = "0";
470
520
  if (state.values.length || state.keys.length || state.scan !== "0") {
471
- next = mcpId(randomUUID());
472
- await mcpStorage2.put(`cursor:${next}`, state, 600);
521
+ next = mcpId(JSON.stringify(state));
522
+ if (!await mcpStorage2.put(`cursor:${next}`, state, RUNTIME_CURSOR_TTL))
523
+ throw new Error("MCP cursor persistence failed");
473
524
  }
474
525
  return {
475
526
  items,
@@ -479,7 +530,17 @@ var readMcpRuntimePage = async (userName, kind, request) => {
479
530
  source: "runtime_storage",
480
531
  window: { startTime: request.startTime, endTime: request.endTime },
481
532
  coverage: "stored_records_only",
482
- note: "Missing records do not prove no evaluation occurred. Continue until cursor is 0; cursors expire after 10 minutes."
533
+ pagination: {
534
+ scanComplete: next === "0",
535
+ storageReads,
536
+ inspected,
537
+ cursorTtlSeconds: RUNTIME_CURSOR_TTL,
538
+ consistency: "mutable_storage_scan",
539
+ order: "storage_scan",
540
+ retentionStartTime: null,
541
+ persistenceCompleteness: "unknown"
542
+ },
543
+ note: "Continue until cursor=0, including empty pages. Cursors are shared across server processes and retained for 24 hours. Retry identical requests after transport failures. If CURSOR_EXPIRED, restart the same window and deduplicate record identities. Storage scans may repeat rows and are not immutable snapshots; missing records do not prove no activity."
483
544
  };
484
545
  };
485
546
  var readMcpSignal = async (userName, signalId, deploymentId, timestamp, strategy) => {
@@ -598,6 +659,7 @@ var readMcpBacktestResult = async (userName, id, offset = 0, limit = 20) => {
598
659
  };
599
660
  export {
600
661
  MCP_JOB_TTL,
662
+ McpCursorError,
601
663
  cancelMcpJob,
602
664
  enqueueMcpJob,
603
665
  isMcpRecord,
@@ -620,5 +682,6 @@ export {
620
682
  readMcpSignal,
621
683
  readMcpStrategies,
622
684
  redactMcpData,
685
+ redactMcpJobLog,
623
686
  requireMcpDeployment
624
687
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tradejs/node",
3
- "version": "3.1.29-beta.272",
3
+ "version": "3.1.29-beta.275",
4
4
  "description": "Node-only runtime for the TradeJS TypeScript framework: strategies, backtests, Pine strategy loading, and plugin registries.",
5
5
  "keywords": [
6
6
  "tradejs",
@@ -105,9 +105,9 @@
105
105
  "dependencies": {
106
106
  "@langchain/core": "^1.2.3",
107
107
  "@langchain/openai": "^1.5.5",
108
- "@tradejs/core": "^3.1.29-beta.272",
109
- "@tradejs/infra": "^3.1.29-beta.272",
110
- "@tradejs/types": "^3.1.29-beta.272",
108
+ "@tradejs/core": "^3.1.29-beta.275",
109
+ "@tradejs/infra": "^3.1.29-beta.275",
110
+ "@tradejs/types": "^3.1.29-beta.275",
111
111
  "@types/node": "^24",
112
112
  "chalk": "4.1.2",
113
113
  "ioredis": "5.11.1",
@@ -120,7 +120,7 @@
120
120
  "typescript": "^5.9"
121
121
  },
122
122
  "devDependencies": {
123
- "@tradejs/indicators": "^3.1.29-beta.272",
123
+ "@tradejs/indicators": "^3.1.29-beta.275",
124
124
  "@tradejs/strategy-adaptive-momentum-ribbon": "3.0.5",
125
125
  "@tradejs/strategy-hyperliquid-consensus": "3.0.5",
126
126
  "@tradejs/strategy-kit": "3.0.6",