@tradejs/cli 3.1.29-beta.297 → 3.1.29-beta.299
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/cli.js +763 -644
- package/dist/lib/mcpJobOutputs.js +133 -0
- package/dist/lib/mcpReplayProcess.js +5 -3
- package/dist/scripts/backtest.js +8 -2
- package/dist/scripts/coverageBackfill.js +11 -7
- package/dist/scripts/mcpWorker.js +151 -43
- package/package.json +7 -7
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __create = Object.create;
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
7
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
+
var __export = (target, all) => {
|
|
9
|
+
for (var name in all)
|
|
10
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
11
|
+
};
|
|
12
|
+
var __copyProps = (to, from, except, desc) => {
|
|
13
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
14
|
+
for (let key of __getOwnPropNames(from))
|
|
15
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
16
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
17
|
+
}
|
|
18
|
+
return to;
|
|
19
|
+
};
|
|
20
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
21
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
22
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
23
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
24
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
25
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
26
|
+
mod
|
|
27
|
+
));
|
|
28
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
29
|
+
|
|
30
|
+
// src/lib/mcpJobOutputs.ts
|
|
31
|
+
var mcpJobOutputs_exports = {};
|
|
32
|
+
__export(mcpJobOutputs_exports, {
|
|
33
|
+
collectMcpJobOutputs: () => collectMcpJobOutputs,
|
|
34
|
+
mcpBacktestResultTimestamp: () => mcpBacktestResultTimestamp
|
|
35
|
+
});
|
|
36
|
+
module.exports = __toCommonJS(mcpJobOutputs_exports);
|
|
37
|
+
var import_promises = require("fs/promises");
|
|
38
|
+
var import_node_path = __toESM(require("path"));
|
|
39
|
+
var import_redis = require("@tradejs/infra/redis");
|
|
40
|
+
var import_diagnostics = require("@tradejs/node/diagnostics");
|
|
41
|
+
var import_mcp = require("@tradejs/node/mcp");
|
|
42
|
+
|
|
43
|
+
// src/lib/runFormatting.ts
|
|
44
|
+
var import_date_fns = require("date-fns");
|
|
45
|
+
var ListIt = require("list-it");
|
|
46
|
+
var createTimestamp = (date) => (0, import_date_fns.format)(date, "yyyyMMddHHmm");
|
|
47
|
+
|
|
48
|
+
// src/lib/mcpJobOutputs.ts
|
|
49
|
+
var mcpBacktestResultTimestamp = (job) => createTimestamp(new Date(job.createdAt));
|
|
50
|
+
var collectEvidence = async (job) => Promise.all(
|
|
51
|
+
(await (0, import_diagnostics.discoverRuntimeEvidenceBundles)((0, import_mcp.mcpJobDirectory)(job))).map(
|
|
52
|
+
async (bundleDir) => {
|
|
53
|
+
const { manifest } = await (0, import_diagnostics.verifyRuntimeEvidenceBundle)(bundleDir);
|
|
54
|
+
if (manifest.userName !== job.userName || manifest.deploymentId !== job.request.deploymentId || manifest.window.startTime !== job.request.startTime || manifest.window.endTime !== job.request.endTime)
|
|
55
|
+
throw new Error("Runtime evidence does not match the job");
|
|
56
|
+
return {
|
|
57
|
+
bundleDir,
|
|
58
|
+
id: (0, import_mcp.mcpId)(`${manifest.artifactId}:${manifest.payload.sha256}`)
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
)
|
|
62
|
+
);
|
|
63
|
+
var collectReplay = async (job) => {
|
|
64
|
+
const sourceDir = import_node_path.default.dirname(String(job.request.verifiedEvidencePath));
|
|
65
|
+
const source = await (0, import_diagnostics.verifyRuntimeEvidenceBundle)(sourceDir);
|
|
66
|
+
const { artifactId, deploymentId, userName, window } = source.manifest;
|
|
67
|
+
if (userName !== job.userName || deploymentId !== job.request.deploymentId || job.request.verifiedEvidenceSha256 !== void 0 && job.request.verifiedEvidenceSha256 !== source.manifest.payload.sha256 || !/^[A-Za-z0-9._-]+$/.test(artifactId) || !/^[A-Za-z0-9._-]+$/.test(deploymentId) || [".", ".."].includes(artifactId) || [".", ".."].includes(deploymentId))
|
|
68
|
+
throw new Error("Runtime evidence does not match the replay job");
|
|
69
|
+
const root = import_node_path.default.join((0, import_mcp.mcpProjectRoot)(), "data/runtime-feedback");
|
|
70
|
+
const receipt = JSON.parse(
|
|
71
|
+
await (0, import_promises.readFile)(
|
|
72
|
+
import_node_path.default.join(root, "completed", deploymentId, `${artifactId}.json`),
|
|
73
|
+
"utf8"
|
|
74
|
+
)
|
|
75
|
+
);
|
|
76
|
+
if (!(0, import_mcp.isMcpRecord)(receipt) || receipt.schemaVersion !== 1 || receipt.runtimeEvidenceArtifactId !== artifactId || typeof receipt.runtimeFeedbackBundle !== "string")
|
|
77
|
+
throw new Error("Invalid replay completion receipt");
|
|
78
|
+
const runId = import_node_path.default.basename(receipt.runtimeFeedbackBundle);
|
|
79
|
+
if (!/^[A-Za-z0-9_-]+$/.test(runId))
|
|
80
|
+
throw new Error("Invalid replay bundle name");
|
|
81
|
+
const day = new Date(window.endTime).toISOString().slice(0, 10).split("-");
|
|
82
|
+
const bundleDir = import_node_path.default.join(
|
|
83
|
+
root,
|
|
84
|
+
"ready",
|
|
85
|
+
deploymentId,
|
|
86
|
+
...day,
|
|
87
|
+
artifactId,
|
|
88
|
+
runId
|
|
89
|
+
);
|
|
90
|
+
const { feedback } = await (0, import_diagnostics.verifyRuntimeFeedbackReplaySource)({
|
|
91
|
+
bundleDir,
|
|
92
|
+
runtimeEvidenceBundleDir: sourceDir
|
|
93
|
+
});
|
|
94
|
+
const { manifest } = feedback;
|
|
95
|
+
return [
|
|
96
|
+
{
|
|
97
|
+
bundleDir,
|
|
98
|
+
id: (0, import_mcp.mcpId)(
|
|
99
|
+
`${manifest.artifactId}:${manifest.payloads.replayEvidence.sha256}`
|
|
100
|
+
)
|
|
101
|
+
}
|
|
102
|
+
];
|
|
103
|
+
};
|
|
104
|
+
var collectMcpJobOutputs = async (job) => {
|
|
105
|
+
if (job.kind === "backtest") {
|
|
106
|
+
const config = job.request.frozenConfigId;
|
|
107
|
+
if (typeof config !== "string")
|
|
108
|
+
throw new Error("Invalid frozen backtest config");
|
|
109
|
+
const timestamp = mcpBacktestResultTimestamp(job);
|
|
110
|
+
if (!await (0, import_redis.getDataStrict)(
|
|
111
|
+
import_redis.redisKeys.backtestResults(job.userName, config, timestamp)
|
|
112
|
+
))
|
|
113
|
+
throw new Error("Backtest result was not saved");
|
|
114
|
+
return { resultId: `${config}:${timestamp}` };
|
|
115
|
+
}
|
|
116
|
+
const bundles = job.kind === "runtime-evidence" ? await collectEvidence(job) : await collectReplay(job);
|
|
117
|
+
if (!bundles.length)
|
|
118
|
+
throw new Error("No verified diagnostic report was saved");
|
|
119
|
+
for (const { id, bundleDir } of bundles) {
|
|
120
|
+
if (!await import_redis.mcpStorage.put(
|
|
121
|
+
(0, import_mcp.mcpArtifactLocationId)(job.userName, id),
|
|
122
|
+
bundleDir,
|
|
123
|
+
import_mcp.MCP_JOB_TTL
|
|
124
|
+
))
|
|
125
|
+
throw new Error("Diagnostic report location could not be saved");
|
|
126
|
+
}
|
|
127
|
+
return { artifactIds: bundles.map(({ id }) => id) };
|
|
128
|
+
};
|
|
129
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
130
|
+
0 && (module.exports = {
|
|
131
|
+
collectMcpJobOutputs,
|
|
132
|
+
mcpBacktestResultTimestamp
|
|
133
|
+
});
|
|
@@ -35,7 +35,6 @@ __export(mcpReplayProcess_exports, {
|
|
|
35
35
|
});
|
|
36
36
|
module.exports = __toCommonJS(mcpReplayProcess_exports);
|
|
37
37
|
var import_node_path = __toESM(require("path"));
|
|
38
|
-
var import_node_crypto = require("crypto");
|
|
39
38
|
var import_promises = __toESM(require("fs/promises"));
|
|
40
39
|
|
|
41
40
|
// src/lib/runtimeEvidenceArtifacts.ts
|
|
@@ -81,6 +80,7 @@ const timer = setInterval(() => {
|
|
|
81
80
|
`;
|
|
82
81
|
var buildMcpReplayProcess = async ({
|
|
83
82
|
evidencePath,
|
|
83
|
+
expectedEvidenceSha256,
|
|
84
84
|
outDir,
|
|
85
85
|
runId,
|
|
86
86
|
projectRoot,
|
|
@@ -91,6 +91,9 @@ var buildMcpReplayProcess = async ({
|
|
|
91
91
|
const bundle = await (0, runtimeEvidenceArtifacts_exports.verifyRuntimeEvidenceBundle)(import_node_path.default.dirname(evidencePath));
|
|
92
92
|
if (import_node_path.default.resolve(evidencePath) !== import_node_path.default.resolve(bundle.payloadPath))
|
|
93
93
|
throw new Error("Replay requires the verified bundle payload");
|
|
94
|
+
const payloadSha256 = bundle.manifest.payload.sha256;
|
|
95
|
+
if (expectedEvidenceSha256 && expectedEvidenceSha256 !== payloadSha256)
|
|
96
|
+
throw new Error("Runtime evidence changed since job submission");
|
|
94
97
|
const relative = import_node_path.default.relative(import_node_path.default.join(projectRoot, "data"), evidencePath);
|
|
95
98
|
if (relative.startsWith("..") || import_node_path.default.isAbsolute(relative))
|
|
96
99
|
throw new Error("Evidence must be inside Project data");
|
|
@@ -100,14 +103,13 @@ var buildMcpReplayProcess = async ({
|
|
|
100
103
|
);
|
|
101
104
|
if (heartbeat.protocolVersion !== 2 || !Number.isFinite(heartbeat.observedAt) || Date.now() - heartbeat.observedAt < 0 || Date.now() - heartbeat.observedAt > 12e4)
|
|
102
105
|
throw new Error("Host replay runner heartbeat is stale");
|
|
103
|
-
const bytes = await import_promises.default.readFile(evidencePath);
|
|
104
106
|
const request = JSON.stringify({
|
|
105
107
|
schemaVersion: 1,
|
|
106
108
|
id: runId,
|
|
107
109
|
evidence: relative,
|
|
108
110
|
artifactId: bundle.manifest.artifactId,
|
|
109
111
|
deploymentId: bundle.manifest.deploymentId,
|
|
110
|
-
payloadSha256
|
|
112
|
+
payloadSha256
|
|
111
113
|
});
|
|
112
114
|
void outDir;
|
|
113
115
|
return {
|
package/dist/scripts/backtest.js
CHANGED
|
@@ -5779,8 +5779,14 @@ var finishBacktest = async (testSuite) => {
|
|
|
5779
5779
|
console.log(import_chalk10.default.gray(`full report: ${markdownReportPath}`));
|
|
5780
5780
|
console.log("");
|
|
5781
5781
|
if (!isFastMode || flags.persistResults) {
|
|
5782
|
-
|
|
5783
|
-
|
|
5782
|
+
const mcpTimestamp = process.env.MCP_WORKER === "true" ? process.env.MCP_BACKTEST_RESULT_TIMESTAMP : void 0;
|
|
5783
|
+
const saveResult = mcpTimestamp ? import_redis6.setDataStrict : import_redis6.setData;
|
|
5784
|
+
await saveResult(
|
|
5785
|
+
import_redis6.redisKeys.backtestResults(
|
|
5786
|
+
userName,
|
|
5787
|
+
flags.config,
|
|
5788
|
+
mcpTimestamp ?? timestamp
|
|
5789
|
+
),
|
|
5784
5790
|
{
|
|
5785
5791
|
config: flags.config,
|
|
5786
5792
|
mode: "config",
|
|
@@ -1578,13 +1578,16 @@ async function fetchTarget(userName, request, target, loaded) {
|
|
|
1578
1578
|
const interval = dataset.interval;
|
|
1579
1579
|
const missing = (time) => time >= day && time < end && !loaded.has(time);
|
|
1580
1580
|
if (dataset.provider === "CoinMarketCap") {
|
|
1581
|
-
|
|
1581
|
+
const params = {
|
|
1582
1582
|
userName,
|
|
1583
1583
|
startMs: day,
|
|
1584
1584
|
preloadStartMs: day,
|
|
1585
1585
|
endMs: end,
|
|
1586
1586
|
repairDataset: dataset.id
|
|
1587
|
-
}
|
|
1587
|
+
};
|
|
1588
|
+
if (resolveCoinMarketCapBackfillWindow(params).fromMs > day)
|
|
1589
|
+
return "outside_configured_history";
|
|
1590
|
+
await backfillCoinMarketCapContext(params);
|
|
1588
1591
|
return;
|
|
1589
1592
|
}
|
|
1590
1593
|
if (dataset.provider === "Coinalyze") {
|
|
@@ -1593,7 +1596,7 @@ async function fetchTarget(userName, request, target, loaded) {
|
|
|
1593
1596
|
userName,
|
|
1594
1597
|
series
|
|
1595
1598
|
);
|
|
1596
|
-
if (!marketSymbol) return;
|
|
1599
|
+
if (!marketSymbol) return "no_matching_market";
|
|
1597
1600
|
const { derivativesRows = [] } = await import_connectors.marketDataProviders.coinalyze.fetchWindow({
|
|
1598
1601
|
symbol: series,
|
|
1599
1602
|
marketSymbol,
|
|
@@ -1685,8 +1688,7 @@ async function fetchTarget(userName, request, target, loaded) {
|
|
|
1685
1688
|
const universe = (0, import_strategies.getBinanceBreadthUniverses)().find(
|
|
1686
1689
|
(item) => item.universe === series
|
|
1687
1690
|
);
|
|
1688
|
-
if (!universe)
|
|
1689
|
-
throw new Error("The historical Binance basket is unavailable.");
|
|
1691
|
+
if (!universe) return "historical_basket_unavailable";
|
|
1690
1692
|
const candlesBySymbol = {};
|
|
1691
1693
|
const warmup = day - 2 * DAY_MS3;
|
|
1692
1694
|
for (let offset = 0; offset < universe.symbols.length; offset += 4) {
|
|
@@ -1715,14 +1717,16 @@ async function main() {
|
|
|
1715
1717
|
);
|
|
1716
1718
|
await (0, import_client3.waitForDbReady)();
|
|
1717
1719
|
const before = await (0, import_dataCoverage.readDataBackfillSlots)(request, target.end);
|
|
1718
|
-
|
|
1719
|
-
await fetchTarget(userName, request, target, before.loaded);
|
|
1720
|
+
const outcome = before.loaded.size < before.expected ? await fetchTarget(userName, request, target, before.loaded) : void 0;
|
|
1720
1721
|
const after = await (0, import_dataCoverage.readDataBackfillSlots)(request, target.end);
|
|
1721
1722
|
const result = {
|
|
1722
1723
|
loaded: after.loaded.size,
|
|
1723
1724
|
added: Math.max(0, after.loaded.size - before.loaded.size),
|
|
1724
1725
|
missing: Math.max(0, after.expected - after.loaded.size)
|
|
1725
1726
|
};
|
|
1727
|
+
if (result.missing) {
|
|
1728
|
+
result.outcome = outcome ?? (result.added ? "some_missing_slots_filled" : "no_missing_slots_filled");
|
|
1729
|
+
}
|
|
1726
1730
|
console.log(`TRADEJS_DATA_BACKFILL ${JSON.stringify(result)}`);
|
|
1727
1731
|
}
|
|
1728
1732
|
// Annotate the CommonJS export names for ESM import in node:
|
|
@@ -35,14 +35,13 @@ __export(mcpWorker_exports, {
|
|
|
35
35
|
});
|
|
36
36
|
module.exports = __toCommonJS(mcpWorker_exports);
|
|
37
37
|
var import_node_child_process = require("child_process");
|
|
38
|
-
var
|
|
39
|
-
var
|
|
40
|
-
var
|
|
41
|
-
var
|
|
38
|
+
var import_node_crypto = require("crypto");
|
|
39
|
+
var import_promises3 = require("fs/promises");
|
|
40
|
+
var import_redis2 = require("@tradejs/infra/redis");
|
|
41
|
+
var import_mcp2 = require("@tradejs/node/mcp");
|
|
42
42
|
|
|
43
43
|
// src/lib/mcpReplayProcess.ts
|
|
44
44
|
var import_node_path = __toESM(require("path"));
|
|
45
|
-
var import_node_crypto = require("crypto");
|
|
46
45
|
var import_promises = __toESM(require("fs/promises"));
|
|
47
46
|
|
|
48
47
|
// src/lib/runtimeEvidenceArtifacts.ts
|
|
@@ -88,6 +87,7 @@ const timer = setInterval(() => {
|
|
|
88
87
|
`;
|
|
89
88
|
var buildMcpReplayProcess = async ({
|
|
90
89
|
evidencePath,
|
|
90
|
+
expectedEvidenceSha256,
|
|
91
91
|
outDir,
|
|
92
92
|
runId,
|
|
93
93
|
projectRoot,
|
|
@@ -98,6 +98,9 @@ var buildMcpReplayProcess = async ({
|
|
|
98
98
|
const bundle = await (0, runtimeEvidenceArtifacts_exports.verifyRuntimeEvidenceBundle)(import_node_path.default.dirname(evidencePath));
|
|
99
99
|
if (import_node_path.default.resolve(evidencePath) !== import_node_path.default.resolve(bundle.payloadPath))
|
|
100
100
|
throw new Error("Replay requires the verified bundle payload");
|
|
101
|
+
const payloadSha256 = bundle.manifest.payload.sha256;
|
|
102
|
+
if (expectedEvidenceSha256 && expectedEvidenceSha256 !== payloadSha256)
|
|
103
|
+
throw new Error("Runtime evidence changed since job submission");
|
|
101
104
|
const relative = import_node_path.default.relative(import_node_path.default.join(projectRoot, "data"), evidencePath);
|
|
102
105
|
if (relative.startsWith("..") || import_node_path.default.isAbsolute(relative))
|
|
103
106
|
throw new Error("Evidence must be inside Project data");
|
|
@@ -107,14 +110,13 @@ var buildMcpReplayProcess = async ({
|
|
|
107
110
|
);
|
|
108
111
|
if (heartbeat.protocolVersion !== 2 || !Number.isFinite(heartbeat.observedAt) || Date.now() - heartbeat.observedAt < 0 || Date.now() - heartbeat.observedAt > 12e4)
|
|
109
112
|
throw new Error("Host replay runner heartbeat is stale");
|
|
110
|
-
const bytes = await import_promises.default.readFile(evidencePath);
|
|
111
113
|
const request = JSON.stringify({
|
|
112
114
|
schemaVersion: 1,
|
|
113
115
|
id: runId,
|
|
114
116
|
evidence: relative,
|
|
115
117
|
artifactId: bundle.manifest.artifactId,
|
|
116
118
|
deploymentId: bundle.manifest.deploymentId,
|
|
117
|
-
payloadSha256
|
|
119
|
+
payloadSha256
|
|
118
120
|
});
|
|
119
121
|
void outDir;
|
|
120
122
|
return {
|
|
@@ -123,14 +125,108 @@ var buildMcpReplayProcess = async ({
|
|
|
123
125
|
};
|
|
124
126
|
};
|
|
125
127
|
|
|
128
|
+
// src/lib/mcpJobOutputs.ts
|
|
129
|
+
var import_promises2 = require("fs/promises");
|
|
130
|
+
var import_node_path2 = __toESM(require("path"));
|
|
131
|
+
var import_redis = require("@tradejs/infra/redis");
|
|
132
|
+
var import_diagnostics = require("@tradejs/node/diagnostics");
|
|
133
|
+
var import_mcp = require("@tradejs/node/mcp");
|
|
134
|
+
|
|
135
|
+
// src/lib/runFormatting.ts
|
|
136
|
+
var import_date_fns = require("date-fns");
|
|
137
|
+
var ListIt = require("list-it");
|
|
138
|
+
var createTimestamp = (date) => (0, import_date_fns.format)(date, "yyyyMMddHHmm");
|
|
139
|
+
|
|
140
|
+
// src/lib/mcpJobOutputs.ts
|
|
141
|
+
var mcpBacktestResultTimestamp = (job) => createTimestamp(new Date(job.createdAt));
|
|
142
|
+
var collectEvidence = async (job) => Promise.all(
|
|
143
|
+
(await (0, import_diagnostics.discoverRuntimeEvidenceBundles)((0, import_mcp.mcpJobDirectory)(job))).map(
|
|
144
|
+
async (bundleDir) => {
|
|
145
|
+
const { manifest } = await (0, import_diagnostics.verifyRuntimeEvidenceBundle)(bundleDir);
|
|
146
|
+
if (manifest.userName !== job.userName || manifest.deploymentId !== job.request.deploymentId || manifest.window.startTime !== job.request.startTime || manifest.window.endTime !== job.request.endTime)
|
|
147
|
+
throw new Error("Runtime evidence does not match the job");
|
|
148
|
+
return {
|
|
149
|
+
bundleDir,
|
|
150
|
+
id: (0, import_mcp.mcpId)(`${manifest.artifactId}:${manifest.payload.sha256}`)
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
)
|
|
154
|
+
);
|
|
155
|
+
var collectReplay = async (job) => {
|
|
156
|
+
const sourceDir = import_node_path2.default.dirname(String(job.request.verifiedEvidencePath));
|
|
157
|
+
const source = await (0, import_diagnostics.verifyRuntimeEvidenceBundle)(sourceDir);
|
|
158
|
+
const { artifactId, deploymentId, userName, window } = source.manifest;
|
|
159
|
+
if (userName !== job.userName || deploymentId !== job.request.deploymentId || job.request.verifiedEvidenceSha256 !== void 0 && job.request.verifiedEvidenceSha256 !== source.manifest.payload.sha256 || !/^[A-Za-z0-9._-]+$/.test(artifactId) || !/^[A-Za-z0-9._-]+$/.test(deploymentId) || [".", ".."].includes(artifactId) || [".", ".."].includes(deploymentId))
|
|
160
|
+
throw new Error("Runtime evidence does not match the replay job");
|
|
161
|
+
const root = import_node_path2.default.join((0, import_mcp.mcpProjectRoot)(), "data/runtime-feedback");
|
|
162
|
+
const receipt = JSON.parse(
|
|
163
|
+
await (0, import_promises2.readFile)(
|
|
164
|
+
import_node_path2.default.join(root, "completed", deploymentId, `${artifactId}.json`),
|
|
165
|
+
"utf8"
|
|
166
|
+
)
|
|
167
|
+
);
|
|
168
|
+
if (!(0, import_mcp.isMcpRecord)(receipt) || receipt.schemaVersion !== 1 || receipt.runtimeEvidenceArtifactId !== artifactId || typeof receipt.runtimeFeedbackBundle !== "string")
|
|
169
|
+
throw new Error("Invalid replay completion receipt");
|
|
170
|
+
const runId = import_node_path2.default.basename(receipt.runtimeFeedbackBundle);
|
|
171
|
+
if (!/^[A-Za-z0-9_-]+$/.test(runId))
|
|
172
|
+
throw new Error("Invalid replay bundle name");
|
|
173
|
+
const day = new Date(window.endTime).toISOString().slice(0, 10).split("-");
|
|
174
|
+
const bundleDir = import_node_path2.default.join(
|
|
175
|
+
root,
|
|
176
|
+
"ready",
|
|
177
|
+
deploymentId,
|
|
178
|
+
...day,
|
|
179
|
+
artifactId,
|
|
180
|
+
runId
|
|
181
|
+
);
|
|
182
|
+
const { feedback } = await (0, import_diagnostics.verifyRuntimeFeedbackReplaySource)({
|
|
183
|
+
bundleDir,
|
|
184
|
+
runtimeEvidenceBundleDir: sourceDir
|
|
185
|
+
});
|
|
186
|
+
const { manifest } = feedback;
|
|
187
|
+
return [
|
|
188
|
+
{
|
|
189
|
+
bundleDir,
|
|
190
|
+
id: (0, import_mcp.mcpId)(
|
|
191
|
+
`${manifest.artifactId}:${manifest.payloads.replayEvidence.sha256}`
|
|
192
|
+
)
|
|
193
|
+
}
|
|
194
|
+
];
|
|
195
|
+
};
|
|
196
|
+
var collectMcpJobOutputs = async (job) => {
|
|
197
|
+
if (job.kind === "backtest") {
|
|
198
|
+
const config = job.request.frozenConfigId;
|
|
199
|
+
if (typeof config !== "string")
|
|
200
|
+
throw new Error("Invalid frozen backtest config");
|
|
201
|
+
const timestamp = mcpBacktestResultTimestamp(job);
|
|
202
|
+
if (!await (0, import_redis.getDataStrict)(
|
|
203
|
+
import_redis.redisKeys.backtestResults(job.userName, config, timestamp)
|
|
204
|
+
))
|
|
205
|
+
throw new Error("Backtest result was not saved");
|
|
206
|
+
return { resultId: `${config}:${timestamp}` };
|
|
207
|
+
}
|
|
208
|
+
const bundles = job.kind === "runtime-evidence" ? await collectEvidence(job) : await collectReplay(job);
|
|
209
|
+
if (!bundles.length)
|
|
210
|
+
throw new Error("No verified diagnostic report was saved");
|
|
211
|
+
for (const { id, bundleDir } of bundles) {
|
|
212
|
+
if (!await import_redis.mcpStorage.put(
|
|
213
|
+
(0, import_mcp.mcpArtifactLocationId)(job.userName, id),
|
|
214
|
+
bundleDir,
|
|
215
|
+
import_mcp.MCP_JOB_TTL
|
|
216
|
+
))
|
|
217
|
+
throw new Error("Diagnostic report location could not be saved");
|
|
218
|
+
}
|
|
219
|
+
return { artifactIds: bundles.map(({ id }) => id) };
|
|
220
|
+
};
|
|
221
|
+
|
|
126
222
|
// src/scripts/mcpWorker.ts
|
|
127
223
|
var pause = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
128
224
|
var leaseKey = "worker:lease";
|
|
129
225
|
var LEASE_SECONDS = 15;
|
|
130
226
|
var JOB_TIMEOUT_MS = 60 * 60 * 1e3;
|
|
131
227
|
var main = async (options = {}) => {
|
|
132
|
-
const owner = (0,
|
|
133
|
-
if (!await
|
|
228
|
+
const owner = (0, import_node_crypto.randomUUID)();
|
|
229
|
+
if (!await import_redis2.mcpStorage.put(leaseKey, owner, LEASE_SECONDS, true))
|
|
134
230
|
throw new Error("Another MCP worker owns this storage");
|
|
135
231
|
let stopping = false;
|
|
136
232
|
let child;
|
|
@@ -145,7 +241,7 @@ var main = async (options = {}) => {
|
|
|
145
241
|
try {
|
|
146
242
|
const root = process.env.MCP_REPLAY_REQUEST_ROOT || `${process.env.PROJECT_CWD || process.cwd()}/data/mcp-replay-requests`;
|
|
147
243
|
const heartbeat2 = JSON.parse(
|
|
148
|
-
await (0,
|
|
244
|
+
await (0, import_promises3.readFile)(`${root}/heartbeat.json`, "utf8")
|
|
149
245
|
);
|
|
150
246
|
replayReady = heartbeat2.protocolVersion === 2 && Number.isFinite(heartbeat2.observedAt) && Date.now() - heartbeat2.observedAt >= 0 && Date.now() - heartbeat2.observedAt < 12e4;
|
|
151
247
|
replayReadiness = replayReady && (heartbeat2.readiness === "ready" || heartbeat2.readiness === "waiting_resources") ? heartbeat2.readiness : "unknown";
|
|
@@ -194,10 +290,10 @@ var main = async (options = {}) => {
|
|
|
194
290
|
if (renewing || leaseLost) return;
|
|
195
291
|
renewing = true;
|
|
196
292
|
try {
|
|
197
|
-
if (!await
|
|
293
|
+
if (!await import_redis2.mcpStorage.compare(leaseKey, owner, owner, LEASE_SECONDS))
|
|
198
294
|
throw new Error("Worker lease lost");
|
|
199
295
|
await updateReplayReady();
|
|
200
|
-
await
|
|
296
|
+
await import_redis2.mcpStorage.put("worker:heartbeat", heartbeatValue(), LEASE_SECONDS);
|
|
201
297
|
} catch {
|
|
202
298
|
leaseLost = true;
|
|
203
299
|
stop();
|
|
@@ -206,17 +302,17 @@ var main = async (options = {}) => {
|
|
|
206
302
|
}
|
|
207
303
|
}, 2e3);
|
|
208
304
|
try {
|
|
209
|
-
manifestHash = await (0,
|
|
305
|
+
manifestHash = await (0, import_mcp2.mcpPackageManifestHash)();
|
|
210
306
|
await updateReplayReady();
|
|
211
|
-
await
|
|
307
|
+
await import_redis2.mcpStorage.put("worker:heartbeat", heartbeatValue(), LEASE_SECONDS);
|
|
212
308
|
let cursor = "0";
|
|
213
309
|
do {
|
|
214
|
-
const page = await
|
|
310
|
+
const page = await import_redis2.mcpStorage.scan("jobs:", cursor);
|
|
215
311
|
cursor = page.cursor;
|
|
216
312
|
for (const key of page.ids) {
|
|
217
|
-
const job = await
|
|
313
|
+
const job = await import_redis2.mcpStorage.get(key);
|
|
218
314
|
if (job?.status === "running")
|
|
219
|
-
await
|
|
315
|
+
await import_redis2.mcpStorage.compare(
|
|
220
316
|
key,
|
|
221
317
|
job,
|
|
222
318
|
{
|
|
@@ -226,7 +322,7 @@ var main = async (options = {}) => {
|
|
|
226
322
|
finishedAt: Date.now(),
|
|
227
323
|
updatedAt: Date.now()
|
|
228
324
|
},
|
|
229
|
-
|
|
325
|
+
import_mcp2.MCP_JOB_TTL
|
|
230
326
|
);
|
|
231
327
|
}
|
|
232
328
|
} while (cursor !== "0" && !stopping);
|
|
@@ -237,10 +333,10 @@ var main = async (options = {}) => {
|
|
|
237
333
|
let queued;
|
|
238
334
|
cursor = "0";
|
|
239
335
|
do {
|
|
240
|
-
const page = await
|
|
336
|
+
const page = await import_redis2.mcpStorage.scan("jobs:", cursor);
|
|
241
337
|
cursor = page.cursor;
|
|
242
338
|
for (const key of page.ids) {
|
|
243
|
-
const job = await
|
|
339
|
+
const job = await import_redis2.mcpStorage.get(key);
|
|
244
340
|
if (job && ["queued", "waiting_resources", "waiting_executor"].includes(
|
|
245
341
|
job.status
|
|
246
342
|
) && (!queued || job.createdAt < queued.createdAt))
|
|
@@ -251,17 +347,17 @@ var main = async (options = {}) => {
|
|
|
251
347
|
await pause(1e3);
|
|
252
348
|
continue;
|
|
253
349
|
}
|
|
254
|
-
const jobKey = (0,
|
|
350
|
+
const jobKey = (0, import_mcp2.mcpJobStorageId)(queued.userName, queued.id);
|
|
255
351
|
const started = {
|
|
256
352
|
...queued,
|
|
257
353
|
status: queued.kind === "runtime-feedback-replay" ? "waiting_executor" : "running",
|
|
258
354
|
...queued.kind === "runtime-feedback-replay" ? {} : { startedAt: Date.now() },
|
|
259
355
|
updatedAt: Date.now()
|
|
260
356
|
};
|
|
261
|
-
if (!await
|
|
357
|
+
if (!await import_redis2.mcpStorage.compare(jobKey, queued, started, import_mcp2.MCP_JOB_TTL))
|
|
262
358
|
continue;
|
|
263
359
|
if (started.kind !== "runtime-feedback-replay" && started.packageManifestSha256 !== manifestHash) {
|
|
264
|
-
await
|
|
360
|
+
await import_redis2.mcpStorage.compare(
|
|
265
361
|
jobKey,
|
|
266
362
|
started,
|
|
267
363
|
{
|
|
@@ -270,19 +366,19 @@ var main = async (options = {}) => {
|
|
|
270
366
|
error: "Package composition changed since submission",
|
|
271
367
|
finishedAt: Date.now()
|
|
272
368
|
},
|
|
273
|
-
|
|
369
|
+
import_mcp2.MCP_JOB_TTL
|
|
274
370
|
);
|
|
275
371
|
continue;
|
|
276
372
|
}
|
|
277
|
-
await (0,
|
|
373
|
+
await (0, import_promises3.mkdir)((0, import_mcp2.mcpJobDirectory)(started), { recursive: true });
|
|
278
374
|
if (started.kind === "backtest")
|
|
279
|
-
await (0,
|
|
280
|
-
|
|
375
|
+
await (0, import_redis2.setDataStrict)(
|
|
376
|
+
import_redis2.redisKeys.backtestConfig(
|
|
281
377
|
started.userName,
|
|
282
378
|
String(started.request.frozenConfigId)
|
|
283
379
|
),
|
|
284
380
|
started.request.resolvedConfig,
|
|
285
|
-
{ expire:
|
|
381
|
+
{ expire: import_mcp2.MCP_JOB_TTL }
|
|
286
382
|
);
|
|
287
383
|
const jobEnv = {
|
|
288
384
|
...process.env,
|
|
@@ -295,6 +391,8 @@ var main = async (options = {}) => {
|
|
|
295
391
|
MAKE_ORDERS: "false",
|
|
296
392
|
TRADEJS_EXTERNAL_ORDER_PLACEMENT: "false"
|
|
297
393
|
};
|
|
394
|
+
if (started.kind === "backtest")
|
|
395
|
+
jobEnv.MCP_BACKTEST_RESULT_TIMESTAMP = mcpBacktestResultTimestamp(started);
|
|
298
396
|
if (started.kind === "runtime-feedback-replay") {
|
|
299
397
|
for (const name of Object.keys(jobEnv)) {
|
|
300
398
|
if (/secret|password|token|api.?key|private.?key|cookie|credential/i.test(
|
|
@@ -320,13 +418,14 @@ var main = async (options = {}) => {
|
|
|
320
418
|
if (started.kind === "runtime-feedback-replay")
|
|
321
419
|
command = await buildMcpReplayProcess({
|
|
322
420
|
evidencePath: String(started.request.verifiedEvidencePath),
|
|
323
|
-
|
|
421
|
+
expectedEvidenceSha256: typeof started.request.verifiedEvidenceSha256 === "string" ? started.request.verifiedEvidenceSha256 : void 0,
|
|
422
|
+
outDir: (0, import_mcp2.mcpJobDirectory)(started),
|
|
324
423
|
runId: started.id,
|
|
325
424
|
projectRoot: process.env.PROJECT_CWD || process.cwd(),
|
|
326
425
|
env: process.env
|
|
327
426
|
});
|
|
328
427
|
} catch (error) {
|
|
329
|
-
await
|
|
428
|
+
await import_redis2.mcpStorage.compare(
|
|
330
429
|
jobKey,
|
|
331
430
|
started,
|
|
332
431
|
{
|
|
@@ -334,9 +433,9 @@ var main = async (options = {}) => {
|
|
|
334
433
|
status: "failed",
|
|
335
434
|
finishedAt: Date.now(),
|
|
336
435
|
updatedAt: Date.now(),
|
|
337
|
-
error: error instanceof Error ? (0,
|
|
436
|
+
error: error instanceof Error ? (0, import_mcp2.redactMcpJobLog)(error.message) : "Replay preparation failed"
|
|
338
437
|
},
|
|
339
|
-
|
|
438
|
+
import_mcp2.MCP_JOB_TTL
|
|
340
439
|
);
|
|
341
440
|
continue;
|
|
342
441
|
}
|
|
@@ -356,7 +455,7 @@ var main = async (options = {}) => {
|
|
|
356
455
|
const logs = [];
|
|
357
456
|
const log = (chunk) => {
|
|
358
457
|
const lines = chunk.toString().replace(/\u001b\[[0-9;]*m/g, "").split(/[\r\n]+/).filter(Boolean);
|
|
359
|
-
logs.push(...lines.map(
|
|
458
|
+
logs.push(...lines.map(import_mcp2.redactMcpJobLog));
|
|
360
459
|
if (logs.length > 150) logs.splice(0, logs.length - 150);
|
|
361
460
|
};
|
|
362
461
|
currentChild.stdout?.on("data", log);
|
|
@@ -364,7 +463,7 @@ var main = async (options = {}) => {
|
|
|
364
463
|
let killAt;
|
|
365
464
|
const monitor = setInterval(async () => {
|
|
366
465
|
try {
|
|
367
|
-
const latest = await
|
|
466
|
+
const latest = await import_redis2.mcpStorage.get(jobKey);
|
|
368
467
|
if (!latest || latest.cancelRequested || stopping || timedOut) {
|
|
369
468
|
killAt ??= Date.now();
|
|
370
469
|
signalChild(Date.now() - killAt > 8e3 ? "SIGKILL" : "SIGTERM");
|
|
@@ -374,7 +473,7 @@ var main = async (options = {}) => {
|
|
|
374
473
|
try {
|
|
375
474
|
const root = process.env.MCP_REPLAY_REQUEST_ROOT || `${process.env.PROJECT_CWD || process.cwd()}/data/mcp-replay-requests`;
|
|
376
475
|
const state = JSON.parse(
|
|
377
|
-
await (0,
|
|
476
|
+
await (0, import_promises3.readFile)(`${root}/${latest.id}.state`, "utf8")
|
|
378
477
|
);
|
|
379
478
|
if (state && typeof state === "object" && "status" in state && (state.status === "waiting_resources" || state.status === "waiting_executor" || state.status === "running"))
|
|
380
479
|
replayStatus = state.status;
|
|
@@ -389,7 +488,7 @@ var main = async (options = {}) => {
|
|
|
389
488
|
}
|
|
390
489
|
}
|
|
391
490
|
if (latest && !leaseLost)
|
|
392
|
-
await
|
|
491
|
+
await import_redis2.mcpStorage.compare(
|
|
393
492
|
jobKey,
|
|
394
493
|
latest,
|
|
395
494
|
{
|
|
@@ -401,7 +500,7 @@ var main = async (options = {}) => {
|
|
|
401
500
|
logs: [...logs],
|
|
402
501
|
updatedAt: Date.now()
|
|
403
502
|
},
|
|
404
|
-
|
|
503
|
+
import_mcp2.MCP_JOB_TTL
|
|
405
504
|
);
|
|
406
505
|
} catch {
|
|
407
506
|
leaseLost = true;
|
|
@@ -418,25 +517,34 @@ var main = async (options = {}) => {
|
|
|
418
517
|
clearTimeout(timeout);
|
|
419
518
|
clearInterval(monitor);
|
|
420
519
|
child = void 0;
|
|
520
|
+
let outputs = {};
|
|
521
|
+
if (!leaseLost && outcome.code === 0 && !outcome.error && !timedOut && !stopping) {
|
|
522
|
+
try {
|
|
523
|
+
outputs = await collectMcpJobOutputs(started);
|
|
524
|
+
} catch {
|
|
525
|
+
outcome.error = "Job finished but its saved output could not be verified";
|
|
526
|
+
}
|
|
527
|
+
}
|
|
421
528
|
if (!leaseLost) {
|
|
422
529
|
for (let attempt = 0; attempt < 4; attempt++) {
|
|
423
|
-
const latest = await
|
|
530
|
+
const latest = await import_redis2.mcpStorage.get(jobKey);
|
|
424
531
|
if (!latest) break;
|
|
425
532
|
const cancelled = latest.cancelRequested;
|
|
426
533
|
const next = {
|
|
427
534
|
...latest,
|
|
428
535
|
logs,
|
|
429
|
-
status: cancelled ? "cancelled" : outcome.code === 0 && !timedOut && !stopping ? "completed" : "failed",
|
|
536
|
+
status: cancelled ? "cancelled" : outcome.code === 0 && !outcome.error && !timedOut && !stopping ? "completed" : "failed",
|
|
537
|
+
...cancelled || stopping ? {} : outputs,
|
|
430
538
|
exitCode: outcome.code,
|
|
431
539
|
updatedAt: Date.now(),
|
|
432
540
|
finishedAt: Date.now(),
|
|
433
|
-
...cancelled ? {} : timedOut ? { error: "Job exceeded one hour" } : stopping ? { error: "Worker stopped; this job was not replayed" } : outcome.code !== 0 ? {
|
|
541
|
+
...cancelled ? {} : timedOut ? { error: "Job exceeded one hour" } : stopping ? { error: "Worker stopped; this job was not replayed" } : outcome.code !== 0 || outcome.error ? {
|
|
434
542
|
error: outcome.error || (started.kind === "runtime-feedback-replay" ? logs.find(
|
|
435
543
|
(line) => line.startsWith("Host replay: ")
|
|
436
544
|
) || logs.at(-1) : void 0) || "Research process failed"
|
|
437
545
|
} : {}
|
|
438
546
|
};
|
|
439
|
-
if (await
|
|
547
|
+
if (await import_redis2.mcpStorage.compare(jobKey, latest, next, import_mcp2.MCP_JOB_TTL))
|
|
440
548
|
break;
|
|
441
549
|
}
|
|
442
550
|
}
|
|
@@ -446,8 +554,8 @@ var main = async (options = {}) => {
|
|
|
446
554
|
options.signal?.removeEventListener("abort", stop);
|
|
447
555
|
process.removeListener("SIGINT", stop);
|
|
448
556
|
process.removeListener("SIGTERM", stop);
|
|
449
|
-
if (await
|
|
450
|
-
await
|
|
557
|
+
if (await import_redis2.mcpStorage.release(leaseKey, owner))
|
|
558
|
+
await import_redis2.mcpStorage.remove("worker:heartbeat");
|
|
451
559
|
}
|
|
452
560
|
if (leaseLost)
|
|
453
561
|
throw new Error("Worker lease lost; active computation was stopped");
|