@tradejs/cli 3.1.29-beta.261 → 3.1.29-beta.263
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/README.md +47 -8
- package/dist/cli.js +990 -1448
- package/dist/lib/aiPocketSearch.js +16 -0
- package/dist/lib/aiPocketSearchCli.js +3 -3
- package/dist/lib/jevMicroReferences.js +73 -0
- package/dist/lib/jevResearch.js +11 -0
- package/dist/lib/runtimeEvidenceArtifacts.js +3 -416
- package/dist/lib/runtimeEvidenceCompositionSnapshots.js +42 -211
- package/dist/lib/runtimeEvidenceCompositions.js +33 -310
- package/dist/lib/runtimeEvidenceDeployment.js +3 -315
- package/dist/lib/runtimeEvidenceProducer.js +3 -77
- package/dist/lib/runtimeEvidenceSync.js +32 -299
- package/dist/lib/runtimeFeedbackArtifacts.js +3 -545
- package/dist/lib/runtimeFeedbackNotification.js +7 -195
- package/dist/lib/runtimeFeedbackReplay.js +26 -525
- package/dist/lib/runtimeFeedbackSync.js +26 -219
- package/dist/lib/runtimeScorecard.js +5 -163
- package/dist/scripts/aiPocketSearch.js +101 -23
- package/dist/scripts/backtest.js +10 -1
- package/dist/scripts/jev.js +11 -0
- package/dist/scripts/mcpWorker.js +273 -0
- package/dist/scripts/replay.js +14 -250
- package/dist/scripts/replayRunner.js +14 -250
- package/dist/scripts/runtimeEvidence.js +47 -597
- package/dist/scripts/runtimeEvidenceSync.js +29 -296
- package/dist/scripts/runtimeFeedbackNotify.js +10 -188
- package/dist/scripts/runtimeFeedbackReplay.js +29 -528
- package/dist/scripts/runtimeFeedbackSync.js +28 -221
- package/dist/scripts/runtimeScorecard.js +88 -503
- package/dist/scripts/signals.js +55 -224
- package/dist/scripts/signalsDaemon.js +55 -224
- package/package.json +9 -8
|
@@ -74,7 +74,7 @@ var resolveAiPocketCadenceProfile = ({
|
|
|
74
74
|
validationRows = [],
|
|
75
75
|
testRows = [],
|
|
76
76
|
mode = "auto",
|
|
77
|
-
validationSplit = 0
|
|
77
|
+
validationSplit = 0,
|
|
78
78
|
minSupport,
|
|
79
79
|
minEvents,
|
|
80
80
|
minValidationSupport,
|
|
@@ -124,8 +124,8 @@ var AI_POCKET_SEARCH_CLI_DECIMAL_DEFAULTS = {
|
|
|
124
124
|
minProfitFactor: "1.2",
|
|
125
125
|
minWinRate: "0",
|
|
126
126
|
minTotalProfit: "0",
|
|
127
|
-
validationSplit: "0
|
|
128
|
-
testSplit: "0"
|
|
127
|
+
validationSplit: "0",
|
|
128
|
+
testSplit: "0.4"
|
|
129
129
|
};
|
|
130
130
|
var splitAiPocketResearchRowsByTimestamp = (rows, validationSplit, testSplit = 0) => {
|
|
131
131
|
const timestamps = [
|
|
@@ -166,11 +166,6 @@ var splitAiPocketResearchRowsByTimestamp = (rows, validationSplit, testSplit = 0
|
|
|
166
166
|
testRows: rows.filter((row) => isIn(testTimestamps, row.timestamp))
|
|
167
167
|
};
|
|
168
168
|
};
|
|
169
|
-
var splitAiPocketCoverageRowsByTimestamp = (rows, family, validationSplit, testSplit = 0) => splitAiPocketResearchRowsByTimestamp(
|
|
170
|
-
rows.filter((row) => row.featureCoverage?.[family] === true),
|
|
171
|
-
validationSplit,
|
|
172
|
-
testSplit
|
|
173
|
-
);
|
|
174
169
|
var sealAiPocketTestPartition = (split, sealed) => {
|
|
175
170
|
const finiteTimestamps = split.testRows.map((row) => row.timestamp).filter((timestamp) => Number.isFinite(timestamp));
|
|
176
171
|
return {
|
|
@@ -436,17 +431,17 @@ var resolveAiPocketSearchCommandOptions = ({
|
|
|
436
431
|
longName: "validationSplit",
|
|
437
432
|
shortName: "V"
|
|
438
433
|
}) ?? flags2.validationSplit,
|
|
439
|
-
0
|
|
440
|
-
) : 0
|
|
434
|
+
0
|
|
435
|
+
) : 0;
|
|
441
436
|
const testSplit = hasCliOption2(argv, "testSplit", "T") ? normalizeRatio(
|
|
442
437
|
readAiPocketSearchCliOption({
|
|
443
438
|
argv,
|
|
444
439
|
longName: "testSplit",
|
|
445
440
|
shortName: "T"
|
|
446
441
|
}) ?? flags2.testSplit,
|
|
447
|
-
0
|
|
448
|
-
) : 0;
|
|
449
|
-
const sealTest = Boolean(flags2.sealTest);
|
|
442
|
+
0.4
|
|
443
|
+
) : 0.4;
|
|
444
|
+
const sealTest = flags2.sealTest == null ? true : Boolean(flags2.sealTest);
|
|
450
445
|
if (sealTest && testSplit <= 0) {
|
|
451
446
|
throw new Error("--sealTest requires a positive --testSplit");
|
|
452
447
|
}
|
|
@@ -2649,6 +2644,22 @@ var buildAiPocketMarkdownReport = ({
|
|
|
2649
2644
|
"",
|
|
2650
2645
|
markdownTable(pocketTableHeaders, pocketRows(pocketSearch.positivePockets)),
|
|
2651
2646
|
"",
|
|
2647
|
+
"## Development Stability (inside discovery, not independent validation)",
|
|
2648
|
+
"",
|
|
2649
|
+
markdownTable(
|
|
2650
|
+
["Condition", "Status", "Block", "Events", "PnL", "PF"],
|
|
2651
|
+
pocketSearch.positivePockets.flatMap(
|
|
2652
|
+
(pocket) => (pocket.stability?.folds ?? []).map((fold, index) => [
|
|
2653
|
+
pocket.condition,
|
|
2654
|
+
pocket.stability?.status,
|
|
2655
|
+
index + 1,
|
|
2656
|
+
fold.summary.events,
|
|
2657
|
+
formatMdNumber(fold.summary.totalProfit),
|
|
2658
|
+
formatMdNumber(fold.summary.profitFactor)
|
|
2659
|
+
])
|
|
2660
|
+
)
|
|
2661
|
+
),
|
|
2662
|
+
"",
|
|
2652
2663
|
"## Top Loss Pockets",
|
|
2653
2664
|
"",
|
|
2654
2665
|
markdownTable(pocketTableHeaders, pocketRows(pocketSearch.negativePockets)),
|
|
@@ -4099,6 +4110,62 @@ var summarizeAiTrainEvaluationsByQualityThreshold = (evaluations, thresholds = [
|
|
|
4099
4110
|
)
|
|
4100
4111
|
}));
|
|
4101
4112
|
|
|
4113
|
+
// src/lib/aiPocketSearch/stability.ts
|
|
4114
|
+
var assessDevelopmentStability = (result, options) => {
|
|
4115
|
+
const minEvents = options.minEventsPerFold ?? 5;
|
|
4116
|
+
const timestamps = [...new Set(options.timestamps)].sort((a, b) => a - b);
|
|
4117
|
+
const boundaries = [0, 1, 2, 3].map(
|
|
4118
|
+
(index) => index === 3 ? timestamps.at(-1) == null ? Number.POSITIVE_INFINITY : timestamps.at(-1) + 1 : timestamps[Math.floor(timestamps.length * index / 3)] ?? Number.POSITIVE_INFINITY
|
|
4119
|
+
);
|
|
4120
|
+
const blocks = [0, 1, 2].map((index) => ({
|
|
4121
|
+
start: boundaries[index],
|
|
4122
|
+
end: boundaries[index + 1],
|
|
4123
|
+
rows: options.developmentRows.filter(
|
|
4124
|
+
(row) => row.timestamp != null && row.timestamp >= boundaries[index] && row.timestamp < boundaries[index + 1]
|
|
4125
|
+
),
|
|
4126
|
+
baseline: options.baselineRows.filter(
|
|
4127
|
+
(row) => row.timestamp != null && row.timestamp >= boundaries[index] && row.timestamp < boundaries[index + 1]
|
|
4128
|
+
)
|
|
4129
|
+
}));
|
|
4130
|
+
for (const pocket of result.positivePockets) {
|
|
4131
|
+
const folds = blocks.map((block) => {
|
|
4132
|
+
const mask = buildPredicateListMask(block.rows, pocket.predicates).mask;
|
|
4133
|
+
const selected = block.rows.filter((_, index) => mask[index] === 1);
|
|
4134
|
+
const summary = summarizeAiPocketRows(selected);
|
|
4135
|
+
const baseline = summarizeAiPocketRows(block.baseline);
|
|
4136
|
+
const objectiveSummary = result.objective === "add-to-gate" ? summarizeAiPocketRows([
|
|
4137
|
+
.../* @__PURE__ */ new Set([...block.baseline, ...selected])
|
|
4138
|
+
]) : summary;
|
|
4139
|
+
return {
|
|
4140
|
+
startTimestamp: Number.isFinite(block.start) ? block.start : null,
|
|
4141
|
+
endTimestamp: Number.isFinite(block.end) ? block.end : null,
|
|
4142
|
+
summary,
|
|
4143
|
+
objectiveSummary,
|
|
4144
|
+
baseline
|
|
4145
|
+
};
|
|
4146
|
+
});
|
|
4147
|
+
const supported = folds.filter((fold) => fold.summary.events >= minEvents);
|
|
4148
|
+
const unstable = supported.some(
|
|
4149
|
+
(fold) => fold.objectiveSummary.totalProfit < (result.objective === "add-to-gate" ? fold.baseline.totalProfit : 0) || (fold.objectiveSummary.profitFactor ?? (fold.objectiveSummary.grossLoss === 0 ? Infinity : 0)) < 1
|
|
4150
|
+
);
|
|
4151
|
+
const status = unstable ? "unstable" : supported.length < 3 ? "insufficient-support" : "stable";
|
|
4152
|
+
pocket.stability = {
|
|
4153
|
+
status,
|
|
4154
|
+
role: "development-only diagnostic",
|
|
4155
|
+
minEventsPerFold: minEvents,
|
|
4156
|
+
folds
|
|
4157
|
+
};
|
|
4158
|
+
if (status !== "stable") {
|
|
4159
|
+
pocket.readiness = "research-only";
|
|
4160
|
+
pocket.readinessReasons.push(`development stability: ${status}`);
|
|
4161
|
+
}
|
|
4162
|
+
}
|
|
4163
|
+
const rank = { stable: 0, "insufficient-support": 1, unstable: 2 };
|
|
4164
|
+
result.positivePockets.sort(
|
|
4165
|
+
(a, b) => rank[a.stability.status] - rank[b.stability.status]
|
|
4166
|
+
);
|
|
4167
|
+
};
|
|
4168
|
+
|
|
4102
4169
|
// src/lib/aiPocketSearch/research.ts
|
|
4103
4170
|
var resolveScopeRows = (rows, scope) => {
|
|
4104
4171
|
if (scope === "approved") {
|
|
@@ -4194,7 +4261,7 @@ var runAiPocketSearchResearch = ({
|
|
|
4194
4261
|
maxEventCountShare,
|
|
4195
4262
|
explicitMaxEventCountShare
|
|
4196
4263
|
});
|
|
4197
|
-
|
|
4264
|
+
const result = searchAiPockets(train, {
|
|
4198
4265
|
minSupport: cadenceProfile.minSupport,
|
|
4199
4266
|
minProfitFactor,
|
|
4200
4267
|
minTotalProfit,
|
|
@@ -4224,6 +4291,17 @@ var runAiPocketSearchResearch = ({
|
|
|
4224
4291
|
progressInterval: 250,
|
|
4225
4292
|
onProgress: (progress) => onProgress?.({ label, progress })
|
|
4226
4293
|
});
|
|
4294
|
+
assessDevelopmentStability(result, {
|
|
4295
|
+
developmentRows: [...train, ...validation],
|
|
4296
|
+
baselineRows: [...trainBaseline, ...validationBaseline],
|
|
4297
|
+
// Every provider/scope uses the same outer development timestamps.
|
|
4298
|
+
timestamps: [
|
|
4299
|
+
...new Set(
|
|
4300
|
+
fullSplit.trainRows.concat(fullSplit.validationRows).map((row) => row.timestamp)
|
|
4301
|
+
)
|
|
4302
|
+
].sort((a, b) => a - b)
|
|
4303
|
+
});
|
|
4304
|
+
return result;
|
|
4227
4305
|
};
|
|
4228
4306
|
const search = runSearch({
|
|
4229
4307
|
label: "full",
|
|
@@ -4243,12 +4321,12 @@ var runAiPocketSearchResearch = ({
|
|
|
4243
4321
|
const coverageSearches = [];
|
|
4244
4322
|
if (coverageMode === "auto") {
|
|
4245
4323
|
for (const family of ["cmc", "coinalyze"]) {
|
|
4246
|
-
const
|
|
4247
|
-
|
|
4248
|
-
|
|
4249
|
-
|
|
4250
|
-
|
|
4251
|
-
|
|
4324
|
+
const covered = (row) => row.featureCoverage?.[family] === true;
|
|
4325
|
+
const cohortSplit = {
|
|
4326
|
+
trainRows: fullSplit.trainRows.filter(covered),
|
|
4327
|
+
validationRows: fullSplit.validationRows.filter(covered),
|
|
4328
|
+
testRows: fullSplit.testRows.filter(covered)
|
|
4329
|
+
};
|
|
4252
4330
|
const sealedCohortSplit = sealAiPocketTestPartition(
|
|
4253
4331
|
cohortSplit,
|
|
4254
4332
|
sealTest
|
|
@@ -4581,18 +4659,18 @@ import_args.default.option(
|
|
|
4581
4659
|
);
|
|
4582
4660
|
import_args.default.option(
|
|
4583
4661
|
["V", "validationSplit"],
|
|
4584
|
-
"
|
|
4662
|
+
"Optional legacy tuning share; default 0, stability is checked inside development",
|
|
4585
4663
|
AI_POCKET_SEARCH_CLI_DECIMAL_DEFAULTS.validationSplit
|
|
4586
4664
|
);
|
|
4587
4665
|
import_args.default.option(
|
|
4588
4666
|
["T", "testSplit"],
|
|
4589
|
-
"Trailing timestamp-grouped
|
|
4667
|
+
"Trailing timestamp-grouped test share; default 0.4 (outer 60/40)",
|
|
4590
4668
|
AI_POCKET_SEARCH_CLI_DECIMAL_DEFAULTS.testSplit
|
|
4591
4669
|
);
|
|
4592
4670
|
import_args.default.option(
|
|
4593
4671
|
["w", "sealTest"],
|
|
4594
4672
|
"Reserve test bounds without exposing its rows or economics to discovery",
|
|
4595
|
-
|
|
4673
|
+
true
|
|
4596
4674
|
);
|
|
4597
4675
|
import_args.default.option(
|
|
4598
4676
|
["N", "minValidationSupport"],
|
package/dist/scripts/backtest.js
CHANGED
|
@@ -74,6 +74,10 @@ var import_userSettings = require("@tradejs/infra/userSettings");
|
|
|
74
74
|
var import_jev3 = require("@tradejs/node/jev");
|
|
75
75
|
var resolveBacktestAiExportEnabled = (flags2) => Boolean(flags2.ai);
|
|
76
76
|
var resolveBacktestJev = async (flags2, userName2, projectRoot2) => {
|
|
77
|
+
if (flags2.jevQuestionSet != null)
|
|
78
|
+
throw new Error(
|
|
79
|
+
"--jevQuestionSet was removed; --jev always uses the current nine-question set"
|
|
80
|
+
);
|
|
77
81
|
if (!flags2.jev) {
|
|
78
82
|
if (flags2.jevRecorded || flags2.jevModelFile)
|
|
79
83
|
throw new Error("Jev options require --jev");
|
|
@@ -4325,6 +4329,11 @@ import_args.default.option(
|
|
|
4325
4329
|
"Skip per-test artifact persistence and keep only in-memory summary/AI-ML dataset output",
|
|
4326
4330
|
false
|
|
4327
4331
|
);
|
|
4332
|
+
import_args.default.option(
|
|
4333
|
+
["Y", "persistResults"],
|
|
4334
|
+
"Persist lightweight result summaries even in fast mode",
|
|
4335
|
+
false
|
|
4336
|
+
);
|
|
4328
4337
|
import_args.default.option(
|
|
4329
4338
|
"researchTrace",
|
|
4330
4339
|
"Write compact setup/entry/exit/skip research trace JSONL",
|
|
@@ -5635,7 +5644,7 @@ var finishBacktest = async (testSuite) => {
|
|
|
5635
5644
|
}
|
|
5636
5645
|
console.log(import_chalk10.default.gray(`full report: ${markdownReportPath}`));
|
|
5637
5646
|
console.log("");
|
|
5638
|
-
if (!isFastMode) {
|
|
5647
|
+
if (!isFastMode || flags.persistResults) {
|
|
5639
5648
|
await (0, import_redis5.setData)(
|
|
5640
5649
|
import_redis5.redisKeys.backtestResults(userName, flags.config, timestamp),
|
|
5641
5650
|
{
|
package/dist/scripts/jev.js
CHANGED
|
@@ -172,6 +172,17 @@ var runJevResearch = async (options) => {
|
|
|
172
172
|
throw new Error(
|
|
173
173
|
"Export exceeds 20000 samples; select a bounded recording directory"
|
|
174
174
|
);
|
|
175
|
+
await (0, import_jev2.evaluateJevInput)({
|
|
176
|
+
input: record.input,
|
|
177
|
+
config: {
|
|
178
|
+
source: "recorded",
|
|
179
|
+
mode: "observe",
|
|
180
|
+
provider: record.provider,
|
|
181
|
+
recordsDir: options.recordsDir
|
|
182
|
+
},
|
|
183
|
+
userName: options.userName,
|
|
184
|
+
projectRoot: options.projectRoot
|
|
185
|
+
});
|
|
175
186
|
const outcome = byRecord.get(record.id);
|
|
176
187
|
rows2.push({
|
|
177
188
|
schema: "tradejs-jev-study/v4",
|
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/scripts/mcpWorker.ts
|
|
21
|
+
var mcpWorker_exports = {};
|
|
22
|
+
__export(mcpWorker_exports, {
|
|
23
|
+
main: () => main
|
|
24
|
+
});
|
|
25
|
+
module.exports = __toCommonJS(mcpWorker_exports);
|
|
26
|
+
var import_node_child_process = require("child_process");
|
|
27
|
+
var import_node_crypto = require("crypto");
|
|
28
|
+
var import_promises = require("fs/promises");
|
|
29
|
+
var import_redis = require("@tradejs/infra/redis");
|
|
30
|
+
var import_mcp = require("@tradejs/node/mcp");
|
|
31
|
+
var pause = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
32
|
+
var leaseKey = "worker:lease";
|
|
33
|
+
var LEASE_SECONDS = 15;
|
|
34
|
+
var JOB_TIMEOUT_MS = 60 * 60 * 1e3;
|
|
35
|
+
var main = async (options = {}) => {
|
|
36
|
+
const owner = (0, import_node_crypto.randomUUID)();
|
|
37
|
+
if (!await import_redis.mcpStorage.put(leaseKey, owner, LEASE_SECONDS, true))
|
|
38
|
+
throw new Error("Another MCP worker owns this storage");
|
|
39
|
+
let stopping = false;
|
|
40
|
+
let child;
|
|
41
|
+
let renewing = false;
|
|
42
|
+
let leaseLost = false;
|
|
43
|
+
let manifestHash = "";
|
|
44
|
+
const signalChild = (signal) => {
|
|
45
|
+
if (!child?.pid) return;
|
|
46
|
+
try {
|
|
47
|
+
process.kill(
|
|
48
|
+
process.platform === "win32" ? child.pid : -child.pid,
|
|
49
|
+
signal
|
|
50
|
+
);
|
|
51
|
+
} catch {
|
|
52
|
+
}
|
|
53
|
+
};
|
|
54
|
+
const heartbeatValue = () => ({
|
|
55
|
+
observedAt: Date.now(),
|
|
56
|
+
packageManifestSha256: manifestHash,
|
|
57
|
+
replayAvailable: Boolean(process.env.MCP_REPLAY_REDIS_HOST)
|
|
58
|
+
});
|
|
59
|
+
const stop = () => {
|
|
60
|
+
stopping = true;
|
|
61
|
+
signalChild("SIGTERM");
|
|
62
|
+
};
|
|
63
|
+
options.signal?.addEventListener("abort", stop, { once: true });
|
|
64
|
+
if (options.signal?.aborted) stop();
|
|
65
|
+
process.once("SIGINT", stop);
|
|
66
|
+
process.once("SIGTERM", stop);
|
|
67
|
+
const heartbeat = setInterval(async () => {
|
|
68
|
+
if (renewing || leaseLost) return;
|
|
69
|
+
renewing = true;
|
|
70
|
+
try {
|
|
71
|
+
if (!await import_redis.mcpStorage.compare(leaseKey, owner, owner, LEASE_SECONDS))
|
|
72
|
+
throw new Error("Worker lease lost");
|
|
73
|
+
await import_redis.mcpStorage.put("worker:heartbeat", heartbeatValue(), LEASE_SECONDS);
|
|
74
|
+
} catch {
|
|
75
|
+
leaseLost = true;
|
|
76
|
+
stop();
|
|
77
|
+
} finally {
|
|
78
|
+
renewing = false;
|
|
79
|
+
}
|
|
80
|
+
}, 2e3);
|
|
81
|
+
try {
|
|
82
|
+
manifestHash = await (0, import_mcp.mcpPackageManifestHash)();
|
|
83
|
+
await import_redis.mcpStorage.put("worker:heartbeat", heartbeatValue(), LEASE_SECONDS);
|
|
84
|
+
let cursor = "0";
|
|
85
|
+
do {
|
|
86
|
+
const page = await import_redis.mcpStorage.scan("jobs:", cursor);
|
|
87
|
+
cursor = page.cursor;
|
|
88
|
+
for (const key of page.ids) {
|
|
89
|
+
const job = await import_redis.mcpStorage.get(key);
|
|
90
|
+
if (job?.status === "running")
|
|
91
|
+
await import_redis.mcpStorage.compare(
|
|
92
|
+
key,
|
|
93
|
+
job,
|
|
94
|
+
{
|
|
95
|
+
...job,
|
|
96
|
+
status: "failed",
|
|
97
|
+
error: "Worker restarted; this job was not replayed",
|
|
98
|
+
finishedAt: Date.now(),
|
|
99
|
+
updatedAt: Date.now()
|
|
100
|
+
},
|
|
101
|
+
import_mcp.MCP_JOB_TTL
|
|
102
|
+
);
|
|
103
|
+
}
|
|
104
|
+
} while (cursor !== "0" && !stopping);
|
|
105
|
+
console.log(
|
|
106
|
+
"MCP worker ready (one job at a time, cached history, no live order commands)"
|
|
107
|
+
);
|
|
108
|
+
while (!stopping) {
|
|
109
|
+
let queued;
|
|
110
|
+
cursor = "0";
|
|
111
|
+
do {
|
|
112
|
+
const page = await import_redis.mcpStorage.scan("jobs:", cursor);
|
|
113
|
+
cursor = page.cursor;
|
|
114
|
+
for (const key of page.ids) {
|
|
115
|
+
const job = await import_redis.mcpStorage.get(key);
|
|
116
|
+
if (job?.status === "queued" && (!queued || job.createdAt < queued.createdAt))
|
|
117
|
+
queued = job;
|
|
118
|
+
}
|
|
119
|
+
} while (cursor !== "0" && !stopping);
|
|
120
|
+
if (!queued) {
|
|
121
|
+
await pause(1e3);
|
|
122
|
+
continue;
|
|
123
|
+
}
|
|
124
|
+
const jobKey = (0, import_mcp.mcpJobStorageId)(queued.userName, queued.id);
|
|
125
|
+
const started = {
|
|
126
|
+
...queued,
|
|
127
|
+
status: "running",
|
|
128
|
+
startedAt: Date.now(),
|
|
129
|
+
updatedAt: Date.now()
|
|
130
|
+
};
|
|
131
|
+
if (!await import_redis.mcpStorage.compare(jobKey, queued, started, import_mcp.MCP_JOB_TTL))
|
|
132
|
+
continue;
|
|
133
|
+
if (started.packageManifestSha256 !== manifestHash) {
|
|
134
|
+
await import_redis.mcpStorage.compare(
|
|
135
|
+
jobKey,
|
|
136
|
+
started,
|
|
137
|
+
{
|
|
138
|
+
...started,
|
|
139
|
+
status: "failed",
|
|
140
|
+
error: "Package composition changed since submission",
|
|
141
|
+
finishedAt: Date.now()
|
|
142
|
+
},
|
|
143
|
+
import_mcp.MCP_JOB_TTL
|
|
144
|
+
);
|
|
145
|
+
continue;
|
|
146
|
+
}
|
|
147
|
+
await (0, import_promises.mkdir)((0, import_mcp.mcpJobDirectory)(started), { recursive: true });
|
|
148
|
+
if (started.kind === "backtest")
|
|
149
|
+
await (0, import_redis.setDataStrict)(
|
|
150
|
+
import_redis.redisKeys.backtestConfig(
|
|
151
|
+
started.userName,
|
|
152
|
+
String(started.request.frozenConfigId)
|
|
153
|
+
),
|
|
154
|
+
started.request.resolvedConfig,
|
|
155
|
+
{ expire: import_mcp.MCP_JOB_TTL }
|
|
156
|
+
);
|
|
157
|
+
const jobEnv = {
|
|
158
|
+
...process.env,
|
|
159
|
+
NODE_OPTIONS: "--max-old-space-size=1024",
|
|
160
|
+
MCP_WORKER: "true",
|
|
161
|
+
BACKTEST_WORKER_HEAP_MB: "768",
|
|
162
|
+
BACKTEST_MAX_PARALLEL: "1",
|
|
163
|
+
AI_ENABLED: "false",
|
|
164
|
+
ML_ENABLED: "false",
|
|
165
|
+
MAKE_ORDERS: "false",
|
|
166
|
+
TRADEJS_EXTERNAL_ORDER_PLACEMENT: "false"
|
|
167
|
+
};
|
|
168
|
+
if (started.kind === "runtime-feedback-replay") {
|
|
169
|
+
for (const name of Object.keys(jobEnv)) {
|
|
170
|
+
if (/secret|token|api.?key|private.?key|cookie|credential/i.test(name))
|
|
171
|
+
delete jobEnv[name];
|
|
172
|
+
}
|
|
173
|
+
Object.assign(jobEnv, {
|
|
174
|
+
DOTENV_CONFIG_PATH: "/dev/null",
|
|
175
|
+
REDIS_HOST: process.env.MCP_REPLAY_REDIS_HOST,
|
|
176
|
+
REDIS_PORT: "6379",
|
|
177
|
+
REDIS_DB: "0",
|
|
178
|
+
RUNTIME_FEEDBACK_ISOLATED_REDIS: "true",
|
|
179
|
+
PGOPTIONS: "-c default_transaction_read_only=on",
|
|
180
|
+
TRADEJS_TIMESCALE_READ_ONLY: "true"
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
child = (0, import_node_child_process.spawn)(process.execPath, [process.argv[1], ...started.args], {
|
|
184
|
+
cwd: process.env.PROJECT_CWD || process.cwd(),
|
|
185
|
+
env: jobEnv,
|
|
186
|
+
detached: process.platform !== "win32",
|
|
187
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
188
|
+
});
|
|
189
|
+
const currentChild = child;
|
|
190
|
+
let timedOut = false;
|
|
191
|
+
const timeout = setTimeout(() => {
|
|
192
|
+
timedOut = true;
|
|
193
|
+
signalChild("SIGTERM");
|
|
194
|
+
}, JOB_TIMEOUT_MS);
|
|
195
|
+
const logs = [];
|
|
196
|
+
const log = (chunk) => {
|
|
197
|
+
const lines = chunk.toString().replace(/\u001b\[[0-9;]*m/g, "").split(/[\r\n]+/).filter(Boolean);
|
|
198
|
+
logs.push(
|
|
199
|
+
...lines.map(
|
|
200
|
+
(line) => line.slice(0, 1e3).replace(
|
|
201
|
+
/((?:secret|password|api.?key|token|authorization)\s*[:=]\s*)\S+/gi,
|
|
202
|
+
"$1[redacted]"
|
|
203
|
+
)
|
|
204
|
+
)
|
|
205
|
+
);
|
|
206
|
+
if (logs.length > 150) logs.splice(0, logs.length - 150);
|
|
207
|
+
};
|
|
208
|
+
currentChild.stdout?.on("data", log);
|
|
209
|
+
currentChild.stderr?.on("data", log);
|
|
210
|
+
let killAt;
|
|
211
|
+
const monitor = setInterval(async () => {
|
|
212
|
+
try {
|
|
213
|
+
const latest = await import_redis.mcpStorage.get(jobKey);
|
|
214
|
+
if (!latest || latest.cancelRequested || stopping || timedOut) {
|
|
215
|
+
killAt ??= Date.now();
|
|
216
|
+
signalChild(Date.now() - killAt > 8e3 ? "SIGKILL" : "SIGTERM");
|
|
217
|
+
}
|
|
218
|
+
if (latest && !leaseLost)
|
|
219
|
+
await import_redis.mcpStorage.compare(
|
|
220
|
+
jobKey,
|
|
221
|
+
latest,
|
|
222
|
+
{ ...latest, logs: [...logs], updatedAt: Date.now() },
|
|
223
|
+
import_mcp.MCP_JOB_TTL
|
|
224
|
+
);
|
|
225
|
+
} catch {
|
|
226
|
+
leaseLost = true;
|
|
227
|
+
stop();
|
|
228
|
+
}
|
|
229
|
+
}, 1e3);
|
|
230
|
+
const outcome = await new Promise((resolve) => {
|
|
231
|
+
currentChild.once(
|
|
232
|
+
"error",
|
|
233
|
+
() => resolve({ code: null, error: "Unable to start research process" })
|
|
234
|
+
);
|
|
235
|
+
currentChild.once("exit", (code) => resolve({ code }));
|
|
236
|
+
});
|
|
237
|
+
clearTimeout(timeout);
|
|
238
|
+
clearInterval(monitor);
|
|
239
|
+
child = void 0;
|
|
240
|
+
if (!leaseLost) {
|
|
241
|
+
for (let attempt = 0; attempt < 4; attempt++) {
|
|
242
|
+
const latest = await import_redis.mcpStorage.get(jobKey);
|
|
243
|
+
if (!latest) break;
|
|
244
|
+
const cancelled = latest.cancelRequested;
|
|
245
|
+
const next = {
|
|
246
|
+
...latest,
|
|
247
|
+
logs,
|
|
248
|
+
status: cancelled ? "cancelled" : outcome.code === 0 && !timedOut && !stopping ? "completed" : "failed",
|
|
249
|
+
exitCode: outcome.code,
|
|
250
|
+
updatedAt: Date.now(),
|
|
251
|
+
finishedAt: Date.now(),
|
|
252
|
+
...cancelled ? {} : timedOut ? { error: "Job exceeded one hour" } : stopping ? { error: "Worker stopped; this job was not replayed" } : outcome.code !== 0 ? { error: outcome.error || "Research process failed" } : {}
|
|
253
|
+
};
|
|
254
|
+
if (await import_redis.mcpStorage.compare(jobKey, latest, next, import_mcp.MCP_JOB_TTL))
|
|
255
|
+
break;
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
} finally {
|
|
260
|
+
clearInterval(heartbeat);
|
|
261
|
+
options.signal?.removeEventListener("abort", stop);
|
|
262
|
+
process.removeListener("SIGINT", stop);
|
|
263
|
+
process.removeListener("SIGTERM", stop);
|
|
264
|
+
if (await import_redis.mcpStorage.release(leaseKey, owner))
|
|
265
|
+
await import_redis.mcpStorage.remove("worker:heartbeat");
|
|
266
|
+
}
|
|
267
|
+
if (leaseLost)
|
|
268
|
+
throw new Error("Worker lease lost; active computation was stopped");
|
|
269
|
+
};
|
|
270
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
271
|
+
0 && (module.exports = {
|
|
272
|
+
main
|
|
273
|
+
});
|